diff options
Diffstat (limited to 'Hotline/macOS/Files')
| -rw-r--r-- | Hotline/macOS/Files/FileDetailsView.swift | 6 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FileItemView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilePreviewImageView.swift | 11 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilePreviewQuickLookView.swift | 131 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilePreviewTextView.swift | 9 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilesView.swift | 103 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FolderItemView.swift | 6 |
7 files changed, 227 insertions, 41 deletions
diff --git a/Hotline/macOS/Files/FileDetailsView.swift b/Hotline/macOS/Files/FileDetailsView.swift index 812f4e5..d001df2 100644 --- a/Hotline/macOS/Files/FileDetailsView.swift +++ b/Hotline/macOS/Files/FileDetailsView.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI struct FileDetailsView: View { - @Environment(Hotline.self) private var model: Hotline + @Environment(HotlineState.self) private var model: HotlineState @Environment(\.presentationMode) var presentationMode var fd: FileDetails @@ -73,8 +73,8 @@ struct FileDetailsView: View { if comment != fd.comment { editedComment = comment } - - model.client.sendSetFileInfo(fileName: fd.name, path: fd.path, fileNewName: editedFilename, comment: editedComment) + + model.setFileInfo(fileName: fd.name, path: fd.path, fileNewName: editedFilename, comment: editedComment) presentationMode.wrappedValue.dismiss() // TODO: Update the file list if the filename was changed diff --git a/Hotline/macOS/Files/FileItemView.swift b/Hotline/macOS/Files/FileItemView.swift index 5da744f..31a8af7 100644 --- a/Hotline/macOS/Files/FileItemView.swift +++ b/Hotline/macOS/Files/FileItemView.swift @@ -1,7 +1,7 @@ import SwiftUI struct FileItemView: View { - @Environment(Hotline.self) private var model: Hotline + @Environment(HotlineState.self) private var model: HotlineState var file: FileInfo let depth: Int diff --git a/Hotline/macOS/Files/FilePreviewImageView.swift b/Hotline/macOS/Files/FilePreviewImageView.swift index 9beeb80..c5899bd 100644 --- a/Hotline/macOS/Files/FilePreviewImageView.swift +++ b/Hotline/macOS/Files/FilePreviewImageView.swift @@ -12,7 +12,7 @@ struct FilePreviewImageView: View { @Binding var info: PreviewFileInfo? - @State var preview: FilePreview? = nil + @State var preview: FilePreviewState? = nil @FocusState private var focusField: FilePreviewFocus? var body: some View { @@ -78,13 +78,16 @@ struct FilePreviewImageView: View { if let info = info { ToolbarItem(placement: .primaryAction) { Button { - let _ = preview?.data?.saveAsFileToDownloads(filename: info.name) + if let fileURL = preview?.fileURL, + let data = try? Data(contentsOf: fileURL) { + let _ = data.saveAsFileToDownloads(filename: info.name) + } } label: { Label("Download Image...", systemImage: "arrow.down") } .help("Download Image") } - + ToolbarItem(placement: .primaryAction) { ShareLink(item: img, preview: SharePreview(info.name, image: img)) { Label("Share Image...", systemImage: "square.and.arrow.up") @@ -96,7 +99,7 @@ struct FilePreviewImageView: View { } .task { if let info = info { - preview = FilePreview(info: info) + preview = FilePreviewState(info: info) preview?.download() } } diff --git a/Hotline/macOS/Files/FilePreviewQuickLookView.swift b/Hotline/macOS/Files/FilePreviewQuickLookView.swift new file mode 100644 index 0000000..0323fdd --- /dev/null +++ b/Hotline/macOS/Files/FilePreviewQuickLookView.swift @@ -0,0 +1,131 @@ +// +// FilePreviewQuickLookView.swift +// Hotline +// +// QuickLook-based file preview window for all supported file types +// + +import SwiftUI +import UniformTypeIdentifiers + +struct FilePreviewQuickLookView: View { + enum FilePreviewFocus: Hashable { + case window + } + + @Environment(\.controlActiveState) private var controlActiveState + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dismiss) var dismiss + + @Binding var info: PreviewFileInfo? + @State var preview: FilePreviewState? = nil + @FocusState private var focusField: FilePreviewFocus? + + var body: some View { + Group { + if preview?.state != .loaded { + VStack(alignment: .center, spacing: 0) { + Spacer() + ProgressView(value: max(0.0, min(1.0, preview?.progress ?? 0.0))) + .focusable(false) + .progressViewStyle(.circular) + .controlSize(.extraLarge) + .frame(maxWidth: 300, alignment: .center) + .padding(.bottom, 48) + Spacer() + } + .background(Color(nsColor: .textBackgroundColor)) + .frame(minWidth: 350, maxWidth: .infinity, minHeight: 150, maxHeight: .infinity) + .padding() + } + else { + if let fileURL = preview?.fileURL { + QuickLookPreviewView(fileURL: fileURL) + .frame(minWidth: 400, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity) + } + else { + VStack(alignment: .center, spacing: 0) { + Spacer() + + Image(systemName: "eye.trianglebadge.exclamationmark") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + .frame(height: 48) + .padding(.bottom) + Group { + Text("This file type is not previewable") + .bold() + Text("Try downloading and opening this file in another application.") + .foregroundStyle(Color.secondary) + } + .font(.system(size: 14.0)) + .frame(maxWidth: 300) + .multilineTextAlignment(.center) + + Spacer() + Spacer() + } + .frame(minWidth: 350, maxWidth: .infinity, minHeight: 150, maxHeight: .infinity) + .padding() + } + } + } + .focusable() + .focusEffectDisabled() + .background(Color(nsColor: .textBackgroundColor)) + .focused($focusField, equals: .window) + .navigationTitle(info?.name ?? "File Preview") + .background( + WindowConfigurator { window in + if let fileURL = preview?.fileURL { + window.representedURL = fileURL + window.standardWindowButton(.documentIconButton)?.isHidden = false + } + } + ) + .toolbar { + if let _ = preview?.fileURL { + if let info = info { + ToolbarItem(placement: .primaryAction) { + Button { + if let fileURL = preview?.fileURL, + let data = try? Data(contentsOf: fileURL) { + let _ = data.saveAsFileToDownloads(filename: info.name) + } + } label: { + Label("Download File...", systemImage: "arrow.down") + } + .help("Download File") + } + } + } + } + .task { + if let info = info { + preview = FilePreviewState(info: info) + preview?.download() + } + } + .onAppear { + if info == nil { + Task { + dismiss() + } + return + } + + focusField = .window + } + .onDisappear { + preview?.cancel() + dismiss() + } + .onChange(of: preview?.state) { + if preview?.state == .failed { + dismiss() + } + } + .preferredColorScheme(.dark) + } +} diff --git a/Hotline/macOS/Files/FilePreviewTextView.swift b/Hotline/macOS/Files/FilePreviewTextView.swift index c286381..4e3a719 100644 --- a/Hotline/macOS/Files/FilePreviewTextView.swift +++ b/Hotline/macOS/Files/FilePreviewTextView.swift @@ -11,7 +11,7 @@ struct FilePreviewTextView: View { @Environment(\.dismiss) var dismiss @Binding var info: PreviewFileInfo? - @State var preview: FilePreview? = nil + @State var preview: FilePreviewState? = nil @FocusState private var focusField: FilePreviewFocus? var body: some View { @@ -89,7 +89,10 @@ struct FilePreviewTextView: View { if let info = info { ToolbarItem(placement: .primaryAction) { Button { - let _ = preview?.data?.saveAsFileToDownloads(filename: info.name) + if let fileURL = preview?.fileURL, + let data = try? Data(contentsOf: fileURL) { + let _ = data.saveAsFileToDownloads(filename: info.name) + } } label: { Label("Save Text File...", systemImage: "square.and.arrow.down") } @@ -100,7 +103,7 @@ struct FilePreviewTextView: View { } .task { if let info = info { - preview = FilePreview(info: info) + preview = FilePreviewState(info: info) preview?.download() } } diff --git a/Hotline/macOS/Files/FilesView.swift b/Hotline/macOS/Files/FilesView.swift index de699ff..b499fe6 100644 --- a/Hotline/macOS/Files/FilesView.swift +++ b/Hotline/macOS/Files/FilesView.swift @@ -7,7 +7,7 @@ import AppKit struct FilesView: View { - @Environment(Hotline.self) private var model: Hotline + @Environment(HotlineState.self) private var model: HotlineState @Environment(\.openWindow) private var openWindow @State private var selection: FileInfo? @@ -15,6 +15,7 @@ struct FilesView: View { @State private var uploadFileSelectorDisplayed: Bool = false @State private var searchText: String = "" @State private var isSearching: Bool = false + @State private var dragOver: Bool = false private var isShowingSearchResults: Bool { switch model.fileSearchStatus { @@ -68,17 +69,18 @@ struct FilesView: View { private func openPreviewWindow(_ previewInfo: PreviewFileInfo) { switch previewInfo.previewType { case .image: - openWindow(id: "preview-image", value: previewInfo) + openWindow(id: "preview-quicklook", value: previewInfo) case .text: - openWindow(id: "preview-text", value: previewInfo) - default: + openWindow(id: "preview-quicklook", value: previewInfo) + case .unknown: + openWindow(id: "preview-quicklook", value: previewInfo) return } } @MainActor private func getFileInfo(_ file: FileInfo) { Task { - if let fileInfo = await model.getFileDetails(file.name, path: file.path) { + if let fileInfo = try? await model.getFileDetails(file.name, path: file.path) { Task { @MainActor in self.fileDetails = fileInfo } @@ -88,10 +90,10 @@ struct FilesView: View { @MainActor private func downloadFile(_ file: FileInfo) { if file.isFolder { - model.downloadFolder(file.name, path: file.path) + model.downloadFolderNew(file.name, path: file.path) } else { - model.downloadFile(file.name, path: file.path) + model.downloadFileNew(file.name, path: file.path) } } @@ -99,7 +101,31 @@ struct FilesView: View { model.uploadFile(url: fileURL, path: path) { info in Task { // Refresh file listing to display newly uploaded file. - let _ = await model.getFileList(path: path) + let _ = try? await model.getFileList(path: path) + } + } + } + + @MainActor private func upload(file fileURL: URL, to path: [String]) { + var fileIsDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: fileURL.path(percentEncoded: false), isDirectory: &fileIsDirectory) else { + return + } + + if fileIsDirectory.boolValue { + self.model.uploadFolder(url: fileURL, path: path, complete: { info in + Task { + // Refresh file listing to display newly uploaded file. + try? await model.getFileList(path: path) + } + }) + } + else { + self.model.uploadFile(url: fileURL, path: path) { info in + Task { + // Refresh file listing to display newly uploaded file. + try? await model.getFileList(path: path) + } } } } @@ -121,15 +147,15 @@ struct FilesView: View { if file.path.count > 1 { parentPath = Array(file.path[0..<file.path.count-1]) } - - if await model.deleteFile(file.name, path: file.path) { - let _ = await model.getFileList(path: parentPath) + + if (try? await model.deleteFile(file.name, path: file.path)) == true { + let _ = try? await model.getFileList(path: parentPath) } } var body: some View { NavigationStack { - List(displayedFiles, id: \.self, selection: $selection) { file in + List(self.displayedFiles, id: \.self, selection: self.$selection) { file in if file.isFolder { FolderItemView(file: file, depth: 0).tag(file.id) } @@ -140,9 +166,36 @@ struct FilesView: View { .environment(\.defaultMinListRowHeight, 28) .listStyle(.inset) .alternatingRowBackgrounds(.enabled) + .onDrop(of: [.fileURL], isTargeted: self.$dragOver) { items in + guard self.model.access?.contains(.canUploadFiles) == true, + let item = items.first, + let identifier = item.registeredTypeIdentifiers.first else { + return false + } + + item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in + DispatchQueue.main.async { + if let urlData = urlData as? Data, + let fileURL = URL(dataRepresentation: urlData, relativeTo: nil, isAbsolute: true) { + + // Access security-scoped resource for drag-and-drop + let didStartAccessing = fileURL.startAccessingSecurityScopedResource() + defer { + if didStartAccessing { + fileURL.stopAccessingSecurityScopedResource() + } + } + + self.upload(file: fileURL, to: []) + } + } + } + + return true + } .task { - if !model.filesLoaded { - let _ = await model.getFileList() + if !self.model.filesLoaded { + let _ = try? await self.model.getFileList() } } .contextMenu(forSelectionType: FileInfo.self) { items in @@ -246,7 +299,7 @@ struct FilesView: View { Label("Preview", systemImage: "eye") } .help("Preview") - .disabled(selection == nil || selection?.isPreviewable == false) + .disabled(selection == nil || selection?.isPreviewable != true) Button { if let selectedFile = selection { @@ -264,10 +317,7 @@ struct FilesView: View { Label("Upload", systemImage: "arrow.up") } .help("Upload") - .disabled( - model.access?.contains(.canUploadFiles) != true || - (model.fileSearchStatus.isActive && !(selection?.isFolder ?? false)) - ) + .disabled(model.access?.contains(.canUploadFiles) != true) Button { if let selectedFile = selection { @@ -284,17 +334,15 @@ struct FilesView: View { .sheet(item: $fileDetails ) { item in FileDetailsView(fd: item) } - .fileImporter(isPresented: $uploadFileSelectorDisplayed, allowedContentTypes: [.data], allowsMultipleSelection: false, onCompletion: { results in + .fileImporter(isPresented: $uploadFileSelectorDisplayed, allowedContentTypes: [.data, .folder], allowsMultipleSelection: false, onCompletion: { results in switch results { case .success(let fileURLS): - guard fileURLS.count > 0 else { + guard fileURLS.count > 0, + let fileURL = fileURLS.first + else { return } - let fileURL = fileURLS.first! - - print(fileURL) - var uploadPath: [String] = [] if let selection = selection { @@ -308,7 +356,8 @@ struct FilesView: View { } print("UPLOAD PATH: \(uploadPath)") - uploadFile(file: fileURL, to: uploadPath) + self.upload(file: fileURL, to: uploadPath) +// uploadFile(file: fileURL, to: uploadPath) case .failure(let error): print(error) @@ -414,5 +463,5 @@ struct FilesView: View { #Preview { FilesView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) + .environment(HotlineState()) } diff --git a/Hotline/macOS/Files/FolderItemView.swift b/Hotline/macOS/Files/FolderItemView.swift index 4a08974..2b1b695 100644 --- a/Hotline/macOS/Files/FolderItemView.swift +++ b/Hotline/macOS/Files/FolderItemView.swift @@ -1,7 +1,7 @@ import SwiftUI struct FolderItemView: View { - @Environment(Hotline.self) private var model: Hotline + @Environment(HotlineState.self) private var model: HotlineState @State var loading = false @State var dragOver = false @@ -20,7 +20,7 @@ struct FolderItemView: View { model.uploadFile(url: fileURL, path: filePath) { info in Task { // Refresh file listing to display newly uploaded file. - let _ = await model.getFileList(path: filePath) + let _ = try? await model.getFileList(path: filePath) } } } @@ -103,7 +103,7 @@ struct FolderItemView: View { if file.expanded && file.fileSize > 0 { Task { loading = true - let _ = await model.getFileList(path: file.path) + let _ = try? await model.getFileList(path: file.path) loading = false } } |