aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/Files
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/macOS/Files')
-rw-r--r--Hotline/macOS/Files/FileDetailsView.swift6
-rw-r--r--Hotline/macOS/Files/FileItemView.swift2
-rw-r--r--Hotline/macOS/Files/FilePreviewImageView.swift11
-rw-r--r--Hotline/macOS/Files/FilePreviewQuickLookView.swift131
-rw-r--r--Hotline/macOS/Files/FilePreviewTextView.swift9
-rw-r--r--Hotline/macOS/Files/FilesView.swift103
-rw-r--r--Hotline/macOS/Files/FolderItemView.swift6
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
}
}