wip
This commit is contained in:
parent
50fba234b0
commit
e21610d425
|
@ -7,4 +7,6 @@ enum DatabaseAction: Codable {
|
|||
case storedChatsLoaded(chats: [Chat])
|
||||
|
||||
case storeMessageFailed(reason: String)
|
||||
|
||||
case updateAttachmentFailed(id: String, reason: String)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import Foundation
|
||||
|
||||
enum FileAction: Stateable {
|
||||
case downloadAttachmentFile(id: String, remotePath: URL)
|
||||
case attachmentFileDownloaded(id: String, localUrl: URL)
|
||||
case downloadingAttachmentFileFailed(id: String, reason: String)
|
||||
|
||||
case createAttachmentThumbnail(id: String, localUrl: URL)
|
||||
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL)
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ extension Database {
|
|||
table.column("localPath", .text)
|
||||
table.column("remotePath", .text)
|
||||
table.column("localThumbnailPath", .text)
|
||||
table.column("downloadFailed", .boolean).notNull().defaults(to: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,43 @@
|
|||
import Foundation
|
||||
|
||||
final class DownloadManager {
|
||||
static let shared = DownloadManager()
|
||||
|
||||
private let urlSession: URLSession
|
||||
private let downloadQueue = DispatchQueue(label: "com.example.downloadQueue")
|
||||
private var activeDownloads = Set<URL>()
|
||||
|
||||
init() {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
urlSession = URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
func download(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
|
||||
let task = urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
|
||||
if let tempLocalUrl = tempLocalUrl, error == nil {
|
||||
do {
|
||||
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
|
||||
completion(nil)
|
||||
} catch let writeError {
|
||||
completion(writeError)
|
||||
}
|
||||
} else {
|
||||
completion(error)
|
||||
func enqueueDownload(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
|
||||
downloadQueue.async {
|
||||
if self.activeDownloads.contains(url) {
|
||||
print("Download for this file is already in queue.")
|
||||
return
|
||||
}
|
||||
|
||||
self.activeDownloads.insert(url)
|
||||
|
||||
let task = self.urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
|
||||
self.downloadQueue.async {
|
||||
self.activeDownloads.remove(url)
|
||||
|
||||
if let tempLocalUrl = tempLocalUrl, error == nil {
|
||||
do {
|
||||
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
|
||||
completion(nil)
|
||||
} catch let writeError {
|
||||
completion(writeError)
|
||||
}
|
||||
} else {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
final class FileManager {}
|
72
ConversationsClassic/AppCore/Files/FileProcessing.swift
Normal file
72
ConversationsClassic/AppCore/Files/FileProcessing.swift
Normal file
|
@ -0,0 +1,72 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class FileProcessing {
|
||||
static let shared = FileProcessing()
|
||||
|
||||
static var fileFolder: URL {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
return documentsURL.appendingPathComponent(Const.fileFolder)
|
||||
}
|
||||
|
||||
func createThumbnail(id: String, localUrl: URL) -> URL? {
|
||||
// make path for thumbnail
|
||||
let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(id).appendingPathExtension("png")
|
||||
|
||||
// check if thumbnail already exists
|
||||
if FileManager.default.fileExists(atPath: thumbnailUrl.path) {
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
// create thumbnail if not exists
|
||||
switch localUrl.lastPathComponent.attachmentType {
|
||||
case .image:
|
||||
guard let image = UIImage(contentsOfFile: localUrl.path) else { return nil }
|
||||
let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
guard let thumbnail = scaleAndCropImage(image, targetSize) else { return nil }
|
||||
guard let data = thumbnail.pngData() else { return nil }
|
||||
do {
|
||||
try data.write(to: thumbnailUrl)
|
||||
return thumbnailUrl
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FileProcessing {
|
||||
func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? {
|
||||
guard let cgImage = img.cgImage else {
|
||||
return nil
|
||||
}
|
||||
let contextImage: UIImage = .init(cgImage: cgImage)
|
||||
var contextSize: CGSize = contextImage.size
|
||||
|
||||
var posX: CGFloat = 0.0
|
||||
var posY: CGFloat = 0.0
|
||||
let cgwidth: CGFloat = size.width
|
||||
let cgheight: CGFloat = size.height
|
||||
|
||||
// Check and handle if the image is wider than the requested size
|
||||
if contextSize.width > contextSize.height {
|
||||
posX = ((contextSize.width - contextSize.height) / 2)
|
||||
contextSize.width = contextSize.height
|
||||
} else if contextSize.width < contextSize.height {
|
||||
// Check and handle if the image is taller than the requested size
|
||||
posY = ((contextSize.height - contextSize.width) / 2)
|
||||
contextSize.height = contextSize.width
|
||||
}
|
||||
|
||||
let rect: CGRect = .init(x: posX, y: posY, width: cgwidth, height: cgheight)
|
||||
guard let contextCg = contextImage.cgImage, let imgRef = contextCg.cropping(to: rect) else {
|
||||
return nil
|
||||
}
|
||||
let image: UIImage = .init(cgImage: imgRef, scale: img.scale, orientation: img.imageOrientation)
|
||||
return image
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import Combine
|
|||
import Foundation
|
||||
import GRDB
|
||||
|
||||
// swiftlint:disable:next type_body_length
|
||||
final class DatabaseMiddleware {
|
||||
static let shared = DatabaseMiddleware()
|
||||
private let database = Database.shared
|
||||
|
@ -277,11 +278,36 @@ final class DatabaseMiddleware {
|
|||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
// MARK: Attachments
|
||||
case .fileAction(.downloadingAttachmentFileFailed(let id, _)):
|
||||
return Future<AppAction, Never> { promise in
|
||||
Task(priority: .background) { [weak self] in
|
||||
guard let database = self?.database else {
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||
)
|
||||
return
|
||||
}
|
||||
do {
|
||||
_ = try database._db.write { db in
|
||||
try Attachment
|
||||
.filter(Column("id") == id)
|
||||
.updateAll(db, Column("downloadFailed").set(to: true))
|
||||
}
|
||||
promise(.success(.empty))
|
||||
} catch {
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
||||
return Future<AppAction, Never> { promise in
|
||||
Task(priority: .background) { [weak self] in
|
||||
guard let database = self?.database else {
|
||||
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||
)
|
||||
return
|
||||
}
|
||||
do {
|
||||
|
@ -292,7 +318,31 @@ final class DatabaseMiddleware {
|
|||
}
|
||||
promise(.success(.empty))
|
||||
} catch {
|
||||
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailUrl)):
|
||||
return Future<AppAction, Never> { promise in
|
||||
Task(priority: .background) { [weak self] in
|
||||
guard let database = self?.database else {
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||
)
|
||||
return
|
||||
}
|
||||
do {
|
||||
_ = try database._db.write { db in
|
||||
try Attachment
|
||||
.filter(Column("id") == id)
|
||||
.updateAll(db, Column("localThumbnailPath").set(to: thumbnailUrl))
|
||||
}
|
||||
promise(.success(.empty))
|
||||
} catch {
|
||||
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,15 +396,3 @@ private extension DatabaseMiddleware {
|
|||
.store(in: &conversationCancellables)
|
||||
}
|
||||
}
|
||||
|
||||
// try db.write { db in
|
||||
// // Update the attachment
|
||||
// var attachment = try Attachment.fetchOne(db, key: attachmentId)!
|
||||
// attachment.someField = newValue
|
||||
// try attachment.update(db)
|
||||
//
|
||||
// // Update the message
|
||||
// var message = try Message.fetchOne(db, key: messageId)!
|
||||
// message.someField = newValue
|
||||
// try message.update(db)
|
||||
// }
|
||||
|
|
|
@ -4,68 +4,51 @@ import UIKit
|
|||
|
||||
final class FileMiddleware {
|
||||
static let shared = FileMiddleware()
|
||||
private var downloader = DownloadManager()
|
||||
|
||||
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
|
||||
switch action {
|
||||
case .conversationAction(.attachmentsUpdated(let attachments)):
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
for attachment in attachments where attachment.localPath == nil {
|
||||
if let remotePath = attachment.remotePath {
|
||||
let localUrl = self.fileFolder.appendingPathComponent(attachment.id)
|
||||
self.downloader.download(from: remotePath, to: localUrl) { error in
|
||||
if error == nil {
|
||||
DispatchQueue.main.async {
|
||||
store.dispatch(.fileAction(.attachmentFileDownloaded(id: attachment.id, localUrl: localUrl)))
|
||||
}
|
||||
}
|
||||
return Future { promise in
|
||||
for attachment in attachments where attachment.localPath == nil && attachment.remotePath != nil {
|
||||
DispatchQueue.main.async {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
store.dispatch(.fileAction(.downloadAttachmentFile(id: attachment.id, remotePath: attachment.remotePath!)))
|
||||
}
|
||||
}
|
||||
promise(.success(.empty))
|
||||
}.eraseToAnyPublisher()
|
||||
|
||||
case .fileAction(.downloadAttachmentFile(let id, let remotePath)):
|
||||
return Future { promise in
|
||||
let localUrl = FileProcessing.fileFolder.appendingPathComponent(id).appendingPathExtension(remotePath.pathExtension)
|
||||
DownloadManager.shared.enqueueDownload(from: remotePath, to: localUrl) { error in
|
||||
DispatchQueue.main.async {
|
||||
if let error {
|
||||
store.dispatch(.fileAction(.downloadingAttachmentFileFailed(id: id, reason: error.localizedDescription)))
|
||||
} else {
|
||||
store.dispatch(.fileAction(.attachmentFileDownloaded(id: id, localUrl: localUrl)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Empty().eraseToAnyPublisher()
|
||||
promise(.success(.empty))
|
||||
}.eraseToAnyPublisher()
|
||||
|
||||
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let attachment = store.state.conversationsState.currentAttachments.first(where: { $0.id == id }) else { return }
|
||||
switch attachment.type {
|
||||
case .image:
|
||||
if let data = try? Data(contentsOf: localUrl), let image = UIImage(data: data) {
|
||||
image.scaleAndCropImage(toExampleSize: CGSizeMake(Const.attachmentPreviewSize, Const.attachmentPreviewSize)) { img in
|
||||
if let img = img, let data = img.jpegData(compressionQuality: 1.0) {
|
||||
let thumbnailUrl = self.fileFolder.appendingPathComponent("\(id)_thumbnail.jpg")
|
||||
do {
|
||||
try data.write(to: thumbnailUrl)
|
||||
DispatchQueue.main.async {
|
||||
store.dispatch(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl)))
|
||||
}
|
||||
} catch {
|
||||
print("Error writing thumbnail: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Just(.fileAction(.createAttachmentThumbnail(id: id, localUrl: localUrl)))
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
case .movie:
|
||||
// self.downloadAndMakeThumbnail(for: attachment)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
case .fileAction(.createAttachmentThumbnail(let id, let localUrl)):
|
||||
return Future { promise in
|
||||
if let thumbnailUrl = FileProcessing.shared.createThumbnail(id: id, localUrl: localUrl) {
|
||||
promise(.success(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl))))
|
||||
} else {
|
||||
promise(.success(.empty))
|
||||
}
|
||||
}
|
||||
return Empty().eraseToAnyPublisher()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
default:
|
||||
return Empty().eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FileMiddleware {
|
||||
var fileFolder: URL {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
return documentsURL.appendingPathComponent(Const.fileFolder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ struct Attachment: DBStorable {
|
|||
let remotePath: URL?
|
||||
let localThumbnailPath: URL?
|
||||
let messageId: String
|
||||
var downloadFailed: Bool = false
|
||||
|
||||
static let message = belongsTo(Message.self)
|
||||
var message: QueryInterfaceRequest<Message> {
|
||||
|
@ -35,14 +36,18 @@ extension String {
|
|||
switch ext {
|
||||
case "mov", "mp4", "avi":
|
||||
return .movie
|
||||
|
||||
case "jpg", "png", "gif":
|
||||
return .image
|
||||
|
||||
case "mp3", "wav", "m4a":
|
||||
return .audio
|
||||
|
||||
case "txt", "doc", "pdf":
|
||||
return .file
|
||||
|
||||
default:
|
||||
return .file // Default to .file if the extension is not recognized
|
||||
return .file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue