This commit is contained in:
fmodf 2024-07-14 12:08:51 +02:00
parent 14a83ca1d8
commit 9c5c54e09e
7 changed files with 47 additions and 35 deletions

View file

@ -1,10 +1,10 @@
import Foundation import Foundation
enum FileAction: Stateable { enum FileAction: Stateable {
case downloadAttachmentFile(id: String, attachmentRemotePath: URL) case downloadAttachmentFile(messageId: String, attachmentRemotePath: URL)
case attachmentFileDownloaded(id: String, localUrl: URL) case attachmentFileDownloaded(messageId: String, localName: String)
case downloadingAttachmentFileFailed(id: String, reason: String) case downloadingAttachmentFileFailed(messageId: String, reason: String)
case createAttachmentThumbnail(id: String, localUrl: URL) case createAttachmentThumbnail(messageId: String, localName: String)
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL) case attachmentThumbnailCreated(messageId: String, thumbnailName: String)
} }

View file

@ -59,9 +59,9 @@ extension Database {
table.column("pending", .boolean).notNull() table.column("pending", .boolean).notNull()
table.column("sentError", .boolean).notNull() table.column("sentError", .boolean).notNull()
table.column("attachmentType", .integer) table.column("attachmentType", .integer)
table.column("attachmentLocalPath", .text) table.column("attachmentLocalName", .text)
table.column("attachmentRemotePath", .text) table.column("attachmentRemotePath", .text)
table.column("attachmentThumbnailPath", .text) table.column("attachmentThumbnailName", .text)
table.column("attachmentDownloadFailed", .boolean).notNull().defaults(to: false) table.column("attachmentDownloadFailed", .boolean).notNull().defaults(to: false)
} }
} }

View file

@ -14,19 +14,18 @@ final class FileProcessing {
return subdirectoryURL return subdirectoryURL
} }
func createThumbnail(localUrl: URL) -> URL? { func createThumbnail(localName: String) -> String? {
let fileExtension = localUrl.pathExtension let thumbnailFileName = "thumb_\(localName)"
let fileNameWithoutExtension = localUrl.deletingPathExtension().lastPathComponent
let thumbnailFileName = fileNameWithoutExtension + "_thumb." + fileExtension
let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(thumbnailFileName) let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(thumbnailFileName)
let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
// check if thumbnail already exists // check if thumbnail already exists
if FileManager.default.fileExists(atPath: thumbnailUrl.path) { if FileManager.default.fileExists(atPath: thumbnailUrl.path) {
return thumbnailUrl return thumbnailFileName
} }
// create thumbnail if not exists // create thumbnail if not exists
switch localUrl.lastPathComponent.attachmentType { switch localName.attachmentType {
case .image: case .image:
guard let image = UIImage(contentsOfFile: localUrl.path) else { return nil } guard let image = UIImage(contentsOfFile: localUrl.path) else { return nil }
let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
@ -34,7 +33,7 @@ final class FileProcessing {
guard let data = thumbnail.pngData() else { return nil } guard let data = thumbnail.pngData() else { return nil }
do { do {
try data.write(to: thumbnailUrl) try data.write(to: thumbnailUrl)
return thumbnailUrl return thumbnailFileName
} catch { } catch {
return nil return nil
} }

View file

@ -35,7 +35,7 @@ final class DatabaseMiddleware {
.store(in: &cancellables) .store(in: &cancellables)
} }
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length cyclomatic_complexity
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
// MARK: Accounts // MARK: Accounts
@ -309,7 +309,7 @@ final class DatabaseMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)): case .fileAction(.attachmentFileDownloaded(let id, let localName)):
return Future<AppAction, Never> { promise in return Future<AppAction, Never> { promise in
Task(priority: .background) { [weak self] in Task(priority: .background) { [weak self] in
guard let database = self?.database else { guard let database = self?.database else {
@ -321,7 +321,7 @@ final class DatabaseMiddleware {
_ = try database._db.write { db in _ = try database._db.write { db in
try Message try Message
.filter(Column("id") == id) .filter(Column("id") == id)
.updateAll(db, Column("attachmentLocalPath").set(to: localUrl), Column("attachmentDownloadFailed").set(to: false)) .updateAll(db, Column("attachmentLocalName").set(to: localName), Column("attachmentDownloadFailed").set(to: false))
} }
promise(.success(.empty)) promise(.success(.empty))
} catch { } catch {
@ -332,7 +332,7 @@ final class DatabaseMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailUrl)): case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailName)):
return Future<AppAction, Never> { promise in return Future<AppAction, Never> { promise in
Task(priority: .background) { [weak self] in Task(priority: .background) { [weak self] in
guard let database = self?.database else { guard let database = self?.database else {
@ -344,7 +344,7 @@ final class DatabaseMiddleware {
_ = try database._db.write { db in _ = try database._db.write { db in
try Message try Message
.filter(Column("id") == id) .filter(Column("id") == id)
.updateAll(db, Column("attachmentThumbnailPath").set(to: thumbnailUrl)) .updateAll(db, Column("attachmentThumbnailName").set(to: thumbnailName))
} }
promise(.success(.empty)) promise(.success(.empty))
} catch { } catch {

View file

@ -21,7 +21,7 @@ final class FileMiddleware {
wSelf.downloadingMessageIDs.insert(message.id) wSelf.downloadingMessageIDs.insert(message.id)
DispatchQueue.main.async { DispatchQueue.main.async {
// swiftlint:disable:next force_unwrapping // swiftlint:disable:next force_unwrapping
store.dispatch(.fileAction(.downloadAttachmentFile(id: message.id, attachmentRemotePath: message.attachmentRemotePath!))) store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!)))
} }
} }
promise(.success(.empty)) promise(.success(.empty))
@ -29,31 +29,32 @@ final class FileMiddleware {
case .fileAction(.downloadAttachmentFile(let id, let attachmentRemotePath)): case .fileAction(.downloadAttachmentFile(let id, let attachmentRemotePath)):
return Future { promise in return Future { promise in
let localUrl = FileProcessing.fileFolder.appendingPathComponent(id).appendingPathExtension(attachmentRemotePath.pathExtension) let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)"
let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in
DispatchQueue.main.async { DispatchQueue.main.async {
if let error { if let error {
store.dispatch(.fileAction(.downloadingAttachmentFileFailed(id: id, reason: error.localizedDescription))) store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription)))
} else { } else {
store.dispatch(.fileAction(.attachmentFileDownloaded(id: id, localUrl: localUrl))) store.dispatch(.fileAction(.attachmentFileDownloaded(messageId: id, localName: localName)))
} }
} }
} }
promise(.success(.empty)) promise(.success(.empty))
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)): case .fileAction(.attachmentFileDownloaded(let id, let localName)):
return Future { [weak self] promise in return Future { [weak self] promise in
self?.downloadingMessageIDs.remove(id) self?.downloadingMessageIDs.remove(id)
promise(.success(.fileAction(.createAttachmentThumbnail(id: id, localUrl: localUrl)))) promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName))))
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .fileAction(.createAttachmentThumbnail(let id, let localUrl)): case .fileAction(.createAttachmentThumbnail(let id, let localName)):
return Future { [weak self] promise in return Future { [weak self] promise in
if let thumbnailUrl = FileProcessing.shared.createThumbnail(localUrl: localUrl) { if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) {
self?.downloadingMessageIDs.remove(id) self?.downloadingMessageIDs.remove(id)
promise(.success(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl)))) promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName))))
} else { } else {
self?.downloadingMessageIDs.remove(id) self?.downloadingMessageIDs.remove(id)
promise(.success(.empty)) promise(.success(.empty))

View file

@ -42,9 +42,9 @@ struct Message: DBStorable, Equatable {
let sentError: Bool let sentError: Bool
var attachmentType: MessageAttachmentType? var attachmentType: MessageAttachmentType?
var attachmentLocalPath: URL? var attachmentLocalName: String?
var attachmentRemotePath: URL? var attachmentRemotePath: URL?
var attachmentThumbnailPath: URL? var attachmentThumbnailName: String?
var attachmentDownloadFailed: Bool = false var attachmentDownloadFailed: Bool = false
} }
@ -96,9 +96,9 @@ extension Message {
pending: false, pending: false,
sentError: false, sentError: false,
attachmentType: nil, attachmentType: nil,
attachmentLocalPath: nil, attachmentLocalName: nil,
attachmentRemotePath: nil, attachmentRemotePath: nil,
attachmentThumbnailPath: nil, attachmentThumbnailName: nil,
attachmentDownloadFailed: false attachmentDownloadFailed: false
) )
if let oob = martinMessage.oob { if let oob = martinMessage.oob {
@ -108,3 +108,15 @@ extension Message {
return msg return msg
} }
} }
extension Message {
var attachmentLocalPath: URL? {
guard let attachmentLocalName = attachmentLocalName else { return nil }
return FileProcessing.fileFolder.appendingPathComponent(attachmentLocalName)
}
var attachmentThumbnailPath: URL? {
guard let attachmentThumbnailName = attachmentThumbnailName else { return nil }
return FileProcessing.fileFolder.appendingPathComponent(attachmentThumbnailName)
}
}

View file

@ -88,8 +88,6 @@ private struct AttachmentView: View {
if let file = message.attachmentLocalPath { if let file = message.attachmentLocalPath {
VideoPlayerView(url: file) VideoPlayerView(url: file)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.cornerRadius(Const.attachmentPreviewSize / 10)
.overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1))
} else { } else {
placeholder placeholder
} }
@ -135,7 +133,7 @@ private struct AttachmentView: View {
} }
.onTapGesture { .onTapGesture {
if let url = message.attachmentRemotePath { if let url = message.attachmentRemotePath {
store.dispatch(.fileAction(.downloadAttachmentFile(id: message.id, attachmentRemotePath: url))) store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: url)))
} }
} }
} }
@ -160,12 +158,14 @@ private struct AttachmentView: View {
} }
} }
// TODO: Make video player better!
private struct VideoPlayerView: UIViewControllerRepresentable { private struct VideoPlayerView: UIViewControllerRepresentable {
let url: URL let url: URL
func makeUIViewController(context _: Context) -> AVPlayerViewController { func makeUIViewController(context _: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController() let controller = AVPlayerViewController()
controller.player = AVPlayer(url: url) controller.player = AVPlayer(url: url)
controller.allowsPictureInPicturePlayback = true
return controller return controller
} }