From 00348340296070012a9f95dec18b4f3ec6fea69e Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 11 Dec 2024 18:59:25 -1000 Subject: [PATCH] fix: Show icons for downloads (#151) --- Reconnect.xcodeproj/project.pbxproj | 4 ++ Reconnect/Model/BrowserModel.swift | 11 +-- Reconnect/Model/FileReference.swift | 41 +++++++++++ Reconnect/Model/Transfer.swift | 8 +-- Reconnect/Model/TransfersModel.swift | 44 ++++++++---- Reconnect/Views/TransferRow.swift | 72 +++++++++++++------ Reconnect/Views/TransfersView.swift | 8 +++ .../ReconnectCore/PLP/FileServer.swift | 18 +++++ 8 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 Reconnect/Model/FileReference.swift diff --git a/Reconnect.xcodeproj/project.pbxproj b/Reconnect.xcodeproj/project.pbxproj index f5fb5eb..eab5d69 100644 --- a/Reconnect.xcodeproj/project.pbxproj +++ b/Reconnect.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ D89071912C4B025D00C11DE8 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = D89071902C4B025D00C11DE8 /* ArgumentParser */; }; D89071932C4B02D900C11DE8 /* ReconnectCore in Frameworks */ = {isa = PBXBuildFile; productRef = D89071922C4B02D900C11DE8 /* ReconnectCore */; }; D89B5E8E2C2AA8680014A5B6 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89B5E8D2C2AA8680014A5B6 /* Sidebar.swift */; }; + D8AB7ED32D0AA1140076E19F /* FileReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AB7ED22D0AA1130076E19F /* FileReference.swift */; }; D8AE29A82CA25A9800116439 /* PsionSoftwareIndex in Frameworks */ = {isa = PBXBuildFile; productRef = D8AE29A72CA25A9800116439 /* PsionSoftwareIndex */; }; D8CBAC652C4A02580035FC3D /* ReconnectCore in Frameworks */ = {isa = PBXBuildFile; productRef = D8CBAC642C4A02580035FC3D /* ReconnectCore */; }; D8D3E79F2C2540D9003E696D /* Interact in Frameworks */ = {isa = PBXBuildFile; productRef = D8D3E79E2C2540D9003E696D /* Interact */; }; @@ -137,6 +138,7 @@ D89071882C4B01FC00C11DE8 /* screenshot-sis */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "screenshot-sis"; sourceTree = BUILT_PRODUCTS_DIR; }; D890718A2C4B01FC00C11DE8 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; D89B5E8D2C2AA8680014A5B6 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; + D8AB7ED22D0AA1130076E19F /* FileReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileReference.swift; sourceTree = ""; }; D8AD8B532C2379A10063A613 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; D8D3E7A02C25410E003E696D /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; D8E31EB42C26E10900350082 /* Licensable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Licensable.swift; sourceTree = ""; }; @@ -224,6 +226,7 @@ D8184C462C253C59008FA79B /* ApplicationModel.swift */, D886DA752C29343900E84BDA /* BrowserModel.swift */, D83B4DE02C2F99C3003C3DC1 /* CheckForUpdatesViewModel.swift */, + D8AB7ED22D0AA1130076E19F /* FileReference.swift */, D8631DB02C374B0A00344DC3 /* Transfer.swift */, D8631DB22C374B4400344DC3 /* TransfersModel.swift */, ); @@ -607,6 +610,7 @@ D83658BB2C29A09300B45693 /* HistoryItemView.swift in Sources */, D8E31EB52C26E10900350082 /* Licensable.swift in Sources */, D886DA742C28CDAD00E84BDA /* BrowserView.swift in Sources */, + D8AB7ED32D0AA1140076E19F /* FileReference.swift in Sources */, D88FA1502C2CE29900805DBD /* ContentView.swift in Sources */, D83B4DE12C2F99C3003C3DC1 /* CheckForUpdatesViewModel.swift in Sources */, D8FFE85F2C49F2FB001F7D8A /* TransfersWindow.swift in Sources */, diff --git a/Reconnect/Model/BrowserModel.swift b/Reconnect/Model/BrowserModel.swift index a33c4ce..25d557d 100644 --- a/Reconnect/Model/BrowserModel.swift +++ b/Reconnect/Model/BrowserModel.swift @@ -215,11 +215,12 @@ class BrowserModel { func download(_ selection: Set? = nil, convertFiles: Bool) { NSWorkspace.shared.open(.transfers) let selection = selection ?? fileSelection - for path in selection { - if path.isWindowsDirectory { - downloadDirectory(path: path, convertFiles: convertFiles) + let files = files.filter { selection.contains($0.id) } + for file in files { + if file.path.isWindowsDirectory { + downloadDirectory(path: file.path, convertFiles: convertFiles) } else { - transfersModel.download(from: path, convertFiles: convertFiles) + transfersModel.download(from: file, convertFiles: convertFiles) } } } @@ -242,7 +243,7 @@ class BrowserModel { if file.isDirectory { try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true) } else { - self.transfersModel.download(from: file.path, to: destinationURL, convertFiles: convertFiles) + self.transfersModel.download(from: file, to: destinationURL, convertFiles: convertFiles) } } } diff --git a/Reconnect/Model/FileReference.swift b/Reconnect/Model/FileReference.swift new file mode 100644 index 0000000..a042204 --- /dev/null +++ b/Reconnect/Model/FileReference.swift @@ -0,0 +1,41 @@ +// Reconnect -- Psion connectivity for macOS +// +// Copyright (C) 2024 Jason Morley +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import Foundation + +import ReconnectCore + +enum FileReference { + + case local(URL) + case remote(FileServer.DirectoryEntry) + +} + +extension FileReference { + + var name: String { + switch self { + case .local(let url): + return url.lastPathComponent + case .remote(let directoryEntry): + return directoryEntry.name + } + } + +} diff --git a/Reconnect/Model/Transfer.swift b/Reconnect/Model/Transfer.swift index 2b41140..e2e97b4 100644 --- a/Reconnect/Model/Transfer.swift +++ b/Reconnect/Model/Transfer.swift @@ -63,7 +63,7 @@ class Transfer: Identifiable { } let id = UUID() - let title: String + let item: FileReference var status: Status @@ -80,9 +80,9 @@ class Transfer: Identifiable { } } - init(title: String, action: @escaping (Transfer) async throws -> Void) { - self.title = title - self.status = .waiting + init(item: FileReference, status: Status = .waiting, action: @escaping ((Transfer) async throws -> Void) = { _ in }) { + self.item = item + self.status = status let task = Task { do { try await action(self) diff --git a/Reconnect/Model/TransfersModel.swift b/Reconnect/Model/TransfersModel.swift index 5ab783c..3773258 100644 --- a/Reconnect/Model/TransfersModel.swift +++ b/Reconnect/Model/TransfersModel.swift @@ -39,24 +39,20 @@ class TransfersModel { init() { } - func add(_ title: String, action: @escaping (Transfer) async throws -> Void) { - transfers.append(Transfer(title: title, action: action)) - } - - func download(from sourcePath: String, to destinationURL: URL? = nil, convertFiles: Bool) { + func download(from source: FileServer.DirectoryEntry, to destinationURL: URL? = nil, convertFiles: Bool) { let fileManager = FileManager.default let downloadsURL = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask)[0] - let filename = sourcePath.lastWindowsPathComponent + let filename = source.path.lastWindowsPathComponent let downloadURL = destinationURL ?? downloadsURL.appendingPathComponent(filename) - print("Downloading file at path '\(sourcePath)' to destination path '\(downloadURL.path)'...") + print("Downloading file at path '\(source.path)' to destination path '\(downloadURL.path)'...") - add(filename) { transfer in + transfers.append(Transfer(item: .remote(source)) { transfer in // Get the file information. - let directoryEntry = try await self.fileServer.getExtendedAttributes(path: sourcePath) + let directoryEntry = try await self.fileServer.getExtendedAttributes(path: source.path) // Perform the file copy. - try await self.fileServer.copyFile(fromRemotePath: sourcePath, toLocalPath: downloadURL.path) { progress, size in + try await self.fileServer.copyFile(fromRemotePath: source.path, toLocalPath: downloadURL.path) { progress, size in transfer.setStatus(.active(Float(progress) / Float(size))) return transfer.isCancelled ? .cancel : .continue } @@ -73,18 +69,18 @@ class TransfersModel { // Mark the transfer as complete. transfer.setStatus(.complete(urls.first)) - } + }) } func upload(from sourceURL: URL, to destinationPath: String) { print("Uploading file at path '\(sourceURL.path)' to destination path '\(destinationPath)'...") - add(sourceURL.lastPathComponent) { transfer in + transfers.append(Transfer(item: .local(sourceURL)) { transfer in try await self.fileServer.copyFile(fromLocalPath: sourceURL.path, toRemotePath: destinationPath) { progress, size in transfer.setStatus(.active(Float(progress) / Float(size))) return transfer.isCancelled ? .cancel : .continue } transfer.setStatus(.complete(nil)) - } + }) } func clear() { @@ -92,3 +88,25 @@ class TransfersModel { } } + +extension TransfersModel { + + func addDemoData() { + let remoteFile = FileServer.DirectoryEntry(path: "D:\\Screenshots\\Thoughts Splash Screen", + name: "Thoughts Splash Screen", + size: 203, + attributes: .normal, + modificationDate: .now, + uid1: .directFileStore, + uid2: .appDllDoc, + uid3: .sketch) + let error = ReconnectError.rfsvError(.init(rawValue: -37)) + transfers.append(Transfer(item: .remote(remoteFile), + status: .waiting)) + transfers.append(Transfer(item: .remote(remoteFile), + status: .failed(error))) + transfers.append(Transfer(item: .local(URL(fileURLWithPath: "/Users/jbmorley/Thoughts Screenshot.png")), + status: .cancelled)) + } + +} diff --git a/Reconnect/Views/TransferRow.swift b/Reconnect/Views/TransferRow.swift index e082f4d..f039250 100644 --- a/Reconnect/Views/TransferRow.swift +++ b/Reconnect/Views/TransferRow.swift @@ -23,14 +23,50 @@ import Interact struct TransferRow: View { let transfer: Transfer + + var image: some View { + switch transfer.item { + case .local: + Image(.fileUnknown16) + .interpolation(.none) + .resizable() + .frame(width: 32, height: 32) + case .remote(let file): + Image(file.fileType.image) + .interpolation(.none) + .resizable() + .frame(width: 32, height: 32) + } + } + + var statusText: String { + switch transfer.status { + case .waiting: + return "Waiting to start…" + case .active(let progress): + return progress.formatted() + case .complete: + return "Complete" + case .cancelled: + return "Cancelled" + case .failed(let error): + return error.localizedDescription + } + + } var body: some View { - HStack { + HStack(spacing: 16.0) { + + self.image VStack(alignment: .leading, spacing: 0) { - - Text(transfer.title) - + + Text(transfer.item.name) + .lineLimit(1) + .truncationMode(.middle) + .horizontalSpace(.trailing) + switch transfer.status { case .waiting: ProgressView(value: 0) @@ -38,25 +74,17 @@ struct TransferRow: View { case .active(let progress): ProgressView(value: progress) .controlSize(.small) - Text(progress.formatted()) - .foregroundStyle(.secondary) - .font(.callout) - case .complete: - Text("Complete") - .foregroundStyle(.secondary) - .font(.callout) - case .cancelled: - Text("Cancelled") - case .failed(let error): - Text(error.localizedDescription) - .foregroundStyle(.secondary) - .font(.callout) + case .complete, .cancelled, .failed: + EmptyView() } - + + Text(statusText) + .lineLimit(1) + .foregroundStyle(.secondary) + .font(.callout) + } - - Spacer() - + switch transfer.status { case .waiting, .active: Button { @@ -89,5 +117,3 @@ struct TransferRow: View { } } - - diff --git a/Reconnect/Views/TransfersView.swift b/Reconnect/Views/TransfersView.swift index 16e2417..5518a00 100644 --- a/Reconnect/Views/TransfersView.swift +++ b/Reconnect/Views/TransfersView.swift @@ -50,7 +50,15 @@ struct TransfersView: View { Divider() HStack { Toggle("Convert Files", isOn: $applicationModel.convertFiles) + Spacer() + +#if DEBUG + Button("Add Demo Data") { + transfersModel.addDemoData() + } +#endif + Button("Clear") { transfersModel.clear() } diff --git a/ReconnectCore/Sources/ReconnectCore/PLP/FileServer.swift b/ReconnectCore/Sources/ReconnectCore/PLP/FileServer.swift index 6ceaa55..efa331e 100644 --- a/ReconnectCore/Sources/ReconnectCore/PLP/FileServer.swift +++ b/ReconnectCore/Sources/ReconnectCore/PLP/FileServer.swift @@ -98,6 +98,24 @@ public class FileServer { public let uid1: UInt32 public let uid2: UInt32 public let uid3: UInt32 + + public init(path: String, + name: String, + size: UInt32, + attributes: FileAttributes, + modificationDate: Date, + uid1: UInt32, + uid2: UInt32, + uid3: UInt32) { + self.path = path + self.name = name + self.size = size + self.attributes = attributes + self.modificationDate = modificationDate + self.uid1 = uid1 + self.uid2 = uid2 + self.uid3 = uid3 + } init(directoryPath: String, entry: PlpDirent) { var entry = entry