diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-07-29 19:07:47 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-07-29 19:07:47 -0700 |
| commit | c6996c3ac262616198e94197d1c6b310d44508ee (patch) | |
| tree | 6a4a3e98b7a2449a3d200bb5f4cc1c3c8b92b4ad | |
| parent | 5726ab32f161e4b62ee0d65303b76d9894769c92 (diff) | |
Add drag and drop suppport for files to upload. Tighten up spacing on heirarchical lists. Add some extra HFS type to UTType conversion. Update to latest packages.
| -rw-r--r-- | Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 8 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineExtensions.swift | 69 | ||||
| -rw-r--r-- | Hotline/Models/FileInfo.swift | 1 | ||||
| -rw-r--r-- | Hotline/Shared/FileIconView.swift | 26 | ||||
| -rw-r--r-- | Hotline/macOS/FileDetailsView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewImageView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewTextView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/FilesView.swift | 196 | ||||
| -rw-r--r-- | Hotline/macOS/NewsView.swift | 4 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 4 |
10 files changed, 241 insertions, 73 deletions
diff --git a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d76a9ac..13c2046 100644 --- a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stevengharris/SplitView", "state" : { - "revision" : "b37d559fd334cd5403433a29a5da6fe78f80714a", - "version" : "3.5.2" + "revision" : "7737a80740919c2fa94a6e64f870d37ef77b9b5f", + "version" : "3.5.3" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/swift-markdown-ui", "state" : { - "revision" : "ae799d015a5374708f7b4c85f3294c05f2a564e2", - "version" : "2.3.0" + "revision" : "9a8119b37e09a770367eeb26e05267c75d854053", + "version" : "2.3.1" } } ], diff --git a/Hotline/Hotline/HotlineExtensions.swift b/Hotline/Hotline/HotlineExtensions.swift index fd57971..81387bf 100644 --- a/Hotline/Hotline/HotlineExtensions.swift +++ b/Hotline/Hotline/HotlineExtensions.swift @@ -108,6 +108,73 @@ extension FileManager { "xml": "R*ch".fourCharCode(), ] + static var HFSTypeToExtension: [String: String] = [ + // Documents + "text": "txt", + "rtf ": "rtf", + "wdbn": "doc", + "w8bn": "docx", + "wprd": "prd", + "wp5 ": "wpd", + "pdf ": "pdf", + "xdoc": "qxd", + "indd": "ind", + + // Spreadsheets + "xls ": "xls", + "xlsx": "xlsx", + "nmbr": "numbers", + + // Presentations + "ppt3": "ppt", + "keyn": "key", + + // Images + "jpeg": "jpg", + "pngf": "png", + "giff": "gif", + "tiff": "tiff", + "ico ": "ico", + "bmpf": "bmp", + "epsf": "eps", + "8bps": "psd", + "pict": "pict", + "tpic": "tga", + "swfl": "swf", + + // Audio + "mp3 ": "mp3", + "m4a ": "m4a", + "caff": "aac", + "wave": "wav", + "aiff": "aiff", + "midi": "midi", + "snd ": "snd", + + // Video + "m4v ": "mp4", + "mpg ": "mpeg", + "mpg2": "mpg2", + "moov": "mov", + "vfw ": "avi", + "wmv ": "wmv", + + // Archives + "zip ": "zip", + "rar ": "rar", + "7zip": "7z", + "tar ": "tar", + "gzip": "gz", + "sitx": "sit", + "sit!": "sit", + "sit5": "sit", + "udif": "dmg", + "cdrw": "cdr", + + // Fonts + "tfil": "ttf", + ] + static var extensionToHFSType: [String: UInt32] = [ // Documents "txt": "TEXT".fourCharCode(), @@ -115,7 +182,7 @@ extension FileManager { "doc": "WDBN".fourCharCode(), "docx": "W8BN".fourCharCode(), "prd": "WPRD".fourCharCode(), - "wpd": "WP5".fourCharCode(), + "wpd": "WP5 ".fourCharCode(), "pdf": "PDF ".fourCharCode(), "qxd": "XDOC".fourCharCode(), "indd": "inDd".fourCharCode(), diff --git a/Hotline/Models/FileInfo.swift b/Hotline/Models/FileInfo.swift index 463f0be..2da7378 100644 --- a/Hotline/Models/FileInfo.swift +++ b/Hotline/Models/FileInfo.swift @@ -27,7 +27,6 @@ import UniformTypeIdentifiers self.isDropboxFolder && (self.name.range(of: "admin", options: [.caseInsensitive]) != nil) } - var expanded: Bool = false var children: [FileInfo]? = nil diff --git a/Hotline/Shared/FileIconView.swift b/Hotline/Shared/FileIconView.swift index ffc9051..d0949fa 100644 --- a/Hotline/Shared/FileIconView.swift +++ b/Hotline/Shared/FileIconView.swift @@ -19,10 +19,11 @@ struct FolderIconView: View { struct FileIconView: View { let filename: String + let fileType: String? #if os(iOS) - private func fileIcon(filename: String) -> Image { - let fileExtension = (filename as NSString).pathExtension + private func fileIcon() -> Image { + let fileExtension = (self.filename as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { if fileType.isSubtype(of: .movie) { return Image(systemName: "play.rectangle") @@ -44,14 +45,29 @@ struct FileIconView: View { return Image(systemName: "doc") } #elseif os(macOS) - private func fileIcon(filename: String) -> Image { - Image(nsImage: NSWorkspace.shared.icon(for: UTType(filenameExtension: (filename as NSString).pathExtension) ?? UTType.content)) + private func fileIcon() -> Image { + let fileExtension = (self.filename as NSString).pathExtension + + if !fileExtension.isEmpty, + let uttype = UTType(filenameExtension: fileExtension) { + return Image(nsImage: NSWorkspace.shared.icon(for: uttype)) + } + else if let fileType = self.fileType, + let fileTypeExtension = FileManager.HFSTypeToExtension[fileType.lowercased()], + let uttype = UTType(filenameExtension: fileTypeExtension) { + return Image(nsImage: NSWorkspace.shared.icon(for: uttype)) + } + else { + return Image(nsImage: NSWorkspace.shared.icon(for: UTType.data)) + } + +// Image(nsImage: NSWorkspace.shared.icon(for: UTType(filenameExtension: (filename as NSString).pathExtension) ?? UTType.content)) } #endif var body: some View { - fileIcon(filename: filename) + fileIcon() .resizable() .scaledToFit() } diff --git a/Hotline/macOS/FileDetailsView.swift b/Hotline/macOS/FileDetailsView.swift index aa7ab09..34e083b 100644 --- a/Hotline/macOS/FileDetailsView.swift +++ b/Hotline/macOS/FileDetailsView.swift @@ -14,7 +14,7 @@ struct FileDetailsView: View { VStack (alignment: .leading){ Form { HStack(alignment: .center){ - FileIconView(filename: fd.name) + FileIconView(filename: fd.name, fileType: nil) .frame(width: 32, height: 32) TextField("", text: $filename) .disabled(!self.canRename()) diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift index b8ad37b..9beeb80 100644 --- a/Hotline/macOS/FilePreviewImageView.swift +++ b/Hotline/macOS/FilePreviewImageView.swift @@ -69,7 +69,7 @@ struct FilePreviewImageView: View { .background(.black) .toolbar { ToolbarItem(placement: .navigation) { - FileIconView(filename: info?.name ?? "") + FileIconView(filename: info?.name ?? "", fileType: nil) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift index 2cb192c..c286381 100644 --- a/Hotline/macOS/FilePreviewTextView.swift +++ b/Hotline/macOS/FilePreviewTextView.swift @@ -80,7 +80,7 @@ struct FilePreviewTextView: View { .navigationTitle(info?.name ?? "File Preview") .toolbar { ToolbarItem(placement: .navigation) { - FileIconView(filename: info?.name ?? "") + FileIconView(filename: info?.name ?? "", fileType: nil) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift index e34c7b5..80d89f9 100644 --- a/Hotline/macOS/FilesView.swift +++ b/Hotline/macOS/FilesView.swift @@ -1,39 +1,51 @@ import SwiftUI import UniformTypeIdentifiers -struct FileView: View { +struct FolderView: View { @Environment(Hotline.self) private var model: Hotline - @State var expanded = false @State var loading = false + @State var dragOver = false var file: FileInfo let depth: Int + @MainActor private func uploadFile(file fileURL: URL) { + var filePath: [String] = [String](self.file.path) + if !self.file.isFolder { + filePath.removeLast() + } + + print("UPLOADING TO PATH: ", filePath) + + model.uploadFile(url: fileURL, path: filePath) { info in + Task { + // Refresh file listing to display newly uploaded file. + let _ = await model.getFileList(path: filePath) + } + } + } + var body: some View { HStack(alignment: .center, spacing: 0) { - if file.isFolder { - Button { - if file.isFolder { - file.expanded.toggle() - } - } label: { - Text(Image(systemName: file.expanded ? "chevron.down" : "chevron.right")) - .bold() - .font(.system(size: 10)) - .opacity(0.5) + Spacer() + .frame(width: CGFloat(depth * (12 + 2))) + + Button { + if file.isFolder { + file.expanded.toggle() } - .buttonStyle(.plain) - .frame(width: 10) - .padding(.leading, 4) - .padding(.trailing, 8) - } - else { - Spacer() - .frame(width: 10) - .padding(.leading, 4) - .padding(.trailing, 8) + } label: { + Text(Image(systemName: file.expanded ? "chevron.down" : "chevron.right")) + .bold() + .font(.system(size: 10)) + .foregroundStyle(dragOver ? Color.white : Color.primary) + .opacity(0.5) } + .buttonStyle(.plain) + .frame(width: 10) + .padding(.leading, 4) + .padding(.trailing, 8) HStack(alignment: .center) { if file.isUnavailable { @@ -53,17 +65,11 @@ struct FileView: View { .scaledToFit() .frame(width: 16, height: 16) } - else if file.isFolder { + else { Image("Folder") .resizable() .scaledToFit() .frame(width: 16, height: 16) -// FolderIconView(dropbox: file.isUploadFolder) -// .frame(width: 16, height: 16) - } - else { - FileIconView(filename: file.name) - .frame(width: 16, height: 16) } } .frame(width: 16) @@ -72,47 +78,123 @@ struct FileView: View { Text(file.name) .lineLimit(1) .truncationMode(.tail) + .foregroundStyle(dragOver ? Color.white : Color.primary) .opacity(file.isUnavailable ? 0.5 : 1.0) - if file.isFolder && loading { + if loading { ProgressView().controlSize(.small).padding([.leading, .trailing], 5) } Spacer() if !file.isUnavailable { - if file.isFolder { - Text(file.fileSize == 0 ? "Empty" : "^[\(file.fileSize) \("file")](inflect: true)") - .foregroundStyle(.secondary) - .lineLimit(1) - .padding(.trailing, 6) - } - else { - Text(formattedFileSize(file.fileSize)) - .foregroundStyle(.secondary) - .lineLimit(1) - .padding(.trailing, 6) - } + Text(file.fileSize == 0 ? "Empty" : "^[\(file.fileSize) \("file")](inflect: true)") + .foregroundStyle(dragOver ? Color.white.opacity(0.75) : Color.secondary) + .lineLimit(1) + .padding(.trailing, 6) } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.leading, CGFloat(depth * (12 + 10))) + .background( + RoundedRectangle(cornerRadius: 4.0) + .fill(dragOver ? Color(nsColor: NSColor.selectedContentBackgroundColor) : Color.clear) + .padding(.horizontal, -6) + .padding(.vertical, -4) + ) .onChange(of: file.expanded) { loading = false - if file.isFolder && file.fileSize > 0 { - if file.expanded { - Task { - loading = true - let _ = await model.getFileList(path: file.path) - loading = false + if file.expanded && file.fileSize > 0 { + Task { + loading = true + let _ = await model.getFileList(path: file.path) + loading = false + } + } + } + .onDrop(of: [.fileURL], isTargeted: $dragOver) { items in + guard let item = items.first, + let identifier = item.registeredTypeIdentifiers.first else { + return false + } + + item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in + print("WHAT?", Thread.isMainThread) + DispatchQueue.main.async { + print("WHAT 2?", Thread.isMainThread) + if let urlData = urlData as? Data, + let fileURL = URL(dataRepresentation: urlData, relativeTo: nil, isAbsolute: true) { + // uploadFile(file: fileURL) } } } + + return true } if file.expanded { ForEach(file.children!, id: \.self) { childFile in - FileView(file: childFile, depth: self.depth + 1).tag(file.id) -// .environment(self.selectedFile) -// .frame(height: 34) + if childFile.isFolder { + FolderView(file: childFile, depth: self.depth + 1).tag(file.id) + } + else { + FileView(file: childFile, depth: self.depth + 1).tag(file.id) + } + } + } + } +} + +struct FileView: View { + @Environment(Hotline.self) private var model: Hotline + + var file: FileInfo + let depth: Int + + var body: some View { + HStack(alignment: .center, spacing: 0) { + Spacer() + .frame(width: CGFloat(depth * (12 + 2))) + + Spacer() + .frame(width: 10) + .padding(.leading, 4) + .padding(.trailing, 8) + + HStack(alignment: .center) { + if file.isUnavailable { + Image(systemName: "questionmark.app.fill") + .frame(width: 16, height: 16) + .opacity(0.5) + } + else { + FileIconView(filename: file.name, fileType: file.type) + .frame(width: 16, height: 16) + } + } + .frame(width: 16) + .padding(.trailing, 6) + + Text(file.name) + .lineLimit(1) + .truncationMode(.tail) + .opacity(file.isUnavailable ? 0.5 : 1.0) + + Spacer() + if !file.isUnavailable { + Text(formattedFileSize(file.fileSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + .padding(.trailing, 6) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + if file.expanded { + ForEach(file.children!, id: \.self) { childFile in + if childFile.isFolder { + FolderView(file: childFile, depth: self.depth + 1).tag(file.id) + } + else { + FileView(file: childFile, depth: self.depth + 1).tag(file.id) + } } } } @@ -120,7 +202,6 @@ struct FileView: View { static let byteFormatter = ByteCountFormatter() private func formattedFileSize(_ fileSize: UInt) -> String { - // let bcf = ByteCountFormatter() FileView.byteFormatter.allowedUnits = [.useAll] FileView.byteFormatter.countStyle = .file return FileView.byteFormatter.string(fromByteCount: Int64(fileSize)) @@ -199,9 +280,14 @@ struct FilesView: View { var body: some View { NavigationStack { List(model.files, id: \.self, selection: $selection) { file in - FileView(file: file, depth: 0).tag(file.id) + if file.isFolder { + FolderView(file: file, depth: 0).tag(file.id) + } + else { + FileView(file: file, depth: 0).tag(file.id) + } } - .environment(\.defaultMinListRowHeight, 34) + .environment(\.defaultMinListRowHeight, 28) .listStyle(.inset) .alternatingRowBackgrounds(.enabled) .task { diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift index 45cb057..91bf2fe 100644 --- a/Hotline/macOS/NewsView.swift +++ b/Hotline/macOS/NewsView.swift @@ -147,7 +147,7 @@ struct NewsView: View { NewsItemView(news: newsItem, depth: 0).tag(newsItem.id) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .environment(\.defaultMinListRowHeight, 34) + .environment(\.defaultMinListRowHeight, 28) .listStyle(.inset) .alternatingRowBackgrounds(.enabled) .contextMenu(forSelectionType: NewsInfo.self) { items in @@ -219,7 +219,7 @@ struct NewsView: View { VStack { HStack { ProgressView { - Text("Loading News") + Text("Loading Newsgroups") } .controlSize(.regular) } diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index 979a233..e9f4a96 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -469,7 +469,7 @@ struct ServerView: View { case .news: NewsView() .navigationTitle(model.serverTitle) - .navigationSubtitle("News") + .navigationSubtitle("Newsgroups") .navigationSplitViewColumnWidth(min: 250, ideal: 500) case .board: MessageBoardView() @@ -609,7 +609,7 @@ struct TransferItemView: View { HStack(alignment: .center, spacing: 5) { HStack(spacing: 0) { Spacer() - FileIconView(filename: transfer.title) + FileIconView(filename: transfer.title, fileType: nil) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) // .padding(.leading, 2) |