diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-01-08 14:45:58 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-01-08 14:45:58 -0800 |
| commit | e9bae0328801d5a056f33beb0f764c6d35e518a2 (patch) | |
| tree | fcf853330f8de68948b089a5b15e2ef4c6dbb47c /Hotline | |
| parent | 4349f50167f50b30d7cd71854882ab33ac6ea5ea (diff) | |
Move to a standard SwiftUI window for preview windows so I can add toolbar items from SwiftUI. Not ideal. Added support for previewing text files too.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Application.swift | 33 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineFileClient.swift | 39 | ||||
| -rw-r--r-- | Hotline/Models/FileInfo.swift | 13 | ||||
| -rw-r--r-- | Hotline/Models/FilePreview.swift | 55 | ||||
| -rw-r--r-- | Hotline/Models/PreviewFileInfo.swift | 20 | ||||
| -rw-r--r-- | Hotline/Shared/AnimatedImageView.swift | 98 | ||||
| -rw-r--r-- | Hotline/Shared/DataAdditions.swift | 23 | ||||
| -rw-r--r-- | Hotline/Shared/FileIconView.swift | 42 | ||||
| -rw-r--r-- | Hotline/Shared/URLAdditions.swift | 42 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewImageView.swift | 124 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewTextView.swift | 128 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewView.swift | 333 | ||||
| -rw-r--r-- | Hotline/macOS/FilesView.swift | 25 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 11 |
14 files changed, 584 insertions, 402 deletions
diff --git a/Hotline/Application.swift b/Hotline/Application.swift index e08695a..4541ea3 100644 --- a/Hotline/Application.swift +++ b/Hotline/Application.swift @@ -1,5 +1,6 @@ import SwiftUI import SwiftData +import UniformTypeIdentifiers @main struct Application: App { @@ -30,8 +31,8 @@ struct Application: App { .defaultPosition(.center) // MARK: Server Window - WindowGroup(id: "server", for: Server.self) { $server in - ServerView(server: $server) + WindowGroup(id: "server", for: Server.self) { server in + ServerView(server: server) .frame(minWidth: 400, minHeight: 300) .environment(preferences) } defaultValue: { @@ -48,19 +49,31 @@ struct Application: App { } } -// WindowGroup(id: "preview", for: PreviewFileInfo.self) { info in -// FilePreviewView(info: info) -// .frame(minWidth: 400, minHeight: 300) -// } -// .defaultSize(width: 750, height: 700) -// .windowStyle(.hiddenTitleBar) -// .aspectRatio(nil, contentMode: .fit) - // MARK: Settings Window Settings { SettingsView() .environment(preferences) } + + // MARK: Image Preview Window + WindowGroup(id: "preview-image", for: PreviewFileInfo.self) { $info in + FilePreviewImageView(info: $info) + } + .windowResizability(.contentSize) + .windowStyle(.titleBar) + .windowToolbarStyle(.unifiedCompact(showsTitle: true)) + .defaultSize(width: 350, height: 150) + .defaultPosition(.center) + + // MARK: Text Preview Window + WindowGroup(id: "preview-text", for: PreviewFileInfo.self) { $info in + FilePreviewTextView(info: $info) + } + .windowResizability(.automatic) + .windowStyle(.titleBar) + .windowToolbarStyle(.unifiedCompact(showsTitle: true)) + .defaultSize(width: 450, height: 550) + .defaultPosition(.center) #endif } diff --git a/Hotline/Hotline/HotlineFileClient.swift b/Hotline/Hotline/HotlineFileClient.swift index 13bd29c..5b2d05a 100644 --- a/Hotline/Hotline/HotlineFileClient.swift +++ b/Hotline/Hotline/HotlineFileClient.swift @@ -263,10 +263,13 @@ class HotlineFileClient { case .ready: self?.status = .connected self?.sendMagic() + case .waiting(let err): + print("HotlineFileClient: Waiting", err) case .cancelled: + print("HotlineFileClient: Cancelled") self?.invalidate() case .failed(let err): - print("HotlineTrackerClient: Connection error \(err)") + print("HotlineFileClient: Connection error \(err)") switch self?.status { case .connecting: print("FAILED TO CONNECT") @@ -344,7 +347,7 @@ class HotlineFileClient { return } - c.receive(minimumIncompleteLength: 1024, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in + c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in guard let self = self else { return } @@ -516,10 +519,11 @@ class HotlineFileClient { private func receivePreviewData() { guard let c = self.connection else { + print("UH OH!") return } - c.receive(minimumIncompleteLength: 1024, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in + c.receive(minimumIncompleteLength: 1, maximumLength: Int(UInt16.max)) { [weak self] (data, context, isComplete, error) in guard let self = self else { return } @@ -534,14 +538,14 @@ class HotlineFileClient { self.fileBytesDownloaded += newData.count self.fileBytes.append(newData) self.status = .progress(Double(self.fileBytesDownloaded) / Double(self.referenceDataSize)) - - print("DOWNLOAD PROGRESS", self.fileBytesDownloaded, self.referenceDataSize, isComplete) + print("HotlineFileClient: Download progress", self.fileBytesDownloaded, self.referenceDataSize, isComplete) } if self.fileBytesDownloaded < Int(self.referenceDataSize) { self.receivePreviewData() } else { + print("HotlineFileClient: Complete") let reference = self.referenceNumber let data = self.fileBytes @@ -557,32 +561,9 @@ class HotlineFileClient { // MARK: - Utility - private func findUniqueFilePath(base: String, at folderURL: URL) -> String { - let fileManager = FileManager.default - var finalName = base - var counter = 2 - - // Helper function to generate a new filename with a counter - func makeFileName() -> String { - let baseName = (base as NSString).deletingPathExtension - let extensionName = (base as NSString).pathExtension - return extensionName.isEmpty ? "\(baseName) \(counter)" : "\(baseName) \(counter).\(extensionName)" - } - - // Check if file exists and append counter until a unique name is found - var filePath = folderURL.appending(component: finalName).path(percentEncoded: false) - while fileManager.fileExists(atPath: filePath) { - finalName = makeFileName() - filePath = folderURL.appending(component: finalName).path(percentEncoded: false) - counter += 1 - } - - return filePath - } - private func prepareDownloadFile(name: String) -> Bool { let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] - let filePath = findUniqueFilePath(base: name, at: folderURL) + let filePath = folderURL.generateUniqueFilePath(filename: name) if FileManager.default.createFile(atPath: filePath, contents: nil) { if let h = FileHandle(forWritingAtPath: filePath) { diff --git a/Hotline/Models/FileInfo.swift b/Hotline/Models/FileInfo.swift index 0cd9b2b..b8d3d12 100644 --- a/Hotline/Models/FileInfo.swift +++ b/Hotline/Models/FileInfo.swift @@ -16,6 +16,19 @@ import UniformTypeIdentifiers var expanded: Bool = false var children: [FileInfo]? = nil + var isPreviewable: Bool { + let fileExtension = (self.name as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + if fileType.isSubtype(of: .image) { + return true + } + else if fileType.isSubtype(of: .text) { + return true + } + } + return false + } + var isImage: Bool { let fileExtension = (self.name as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { diff --git a/Hotline/Models/FilePreview.swift b/Hotline/Models/FilePreview.swift index 3a1d225..d558c4c 100644 --- a/Hotline/Models/FilePreview.swift +++ b/Hotline/Models/FilePreview.swift @@ -8,6 +8,12 @@ enum FilePreviewState: Equatable { case failed } +enum FilePreviewType: Equatable { + case unknown + case image + case text +} + @Observable final class FilePreview: HotlineFileClientDelegate { @ObservationIgnored let info: PreviewFileInfo @@ -16,10 +22,30 @@ final class FilePreview: HotlineFileClientDelegate { var state: FilePreviewState = .unloaded var progress: Double = 0.0 - #if os(macOS) - var image: NSImage? + var data: Data? = nil + + #if os(iOS) + var image: UIImage? = nil + #elseif os(macOS) + var image: NSImage? = nil #endif + var text: String? = nil + var styledText: NSAttributedString? = nil + + var previewType: FilePreviewType { + let fileExtension = (info.name as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + if fileType.isSubtype(of: .image) { + return .image + } + else if fileType.isSubtype(of: .text) { + return .text + } + } + return .unknown + } + init(info: PreviewFileInfo) { self.info = info @@ -62,16 +88,25 @@ final class FilePreview: HotlineFileClientDelegate { func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) { self.state = .loaded + self.data = data - let fileExtension = (info.name as NSString).pathExtension - if let fileType = UTType(filenameExtension: fileExtension) { - if fileType.isSubtype(of: .image) { - #if os(iOS) -// self.image = UIImage(data: data) - #elseif os(macOS) - self.image = NSImage(data: data) - #endif + switch self.previewType { + case .image: + #if os(iOS) + self.image = UIImage(data: data) + #elseif os(macOS) + self.image = NSImage(data: data) + #endif + case .text: + let encoding: UInt = NSString.stringEncoding(for: data, convertedString: nil, usedLossyConversion: nil) + if encoding != 0 { + self.text = String(data: data, encoding: String.Encoding(rawValue: encoding)) + } + else { + self.text = String(data: data, encoding: .utf8) } + case .unknown: + return } } } diff --git a/Hotline/Models/PreviewFileInfo.swift b/Hotline/Models/PreviewFileInfo.swift index 9a4c980..7dd692f 100644 --- a/Hotline/Models/PreviewFileInfo.swift +++ b/Hotline/Models/PreviewFileInfo.swift @@ -1,3 +1,10 @@ +import UniformTypeIdentifiers + +enum PreviewFileType: Equatable { + case unknown + case image + case text +} struct PreviewFileInfo: Identifiable, Codable { var id: UInt32 @@ -5,6 +12,19 @@ struct PreviewFileInfo: Identifiable, Codable { var port: Int var size: Int var name: String + + var previewType: FilePreviewType { + let fileExtension = (self.name as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + if fileType.isSubtype(of: .image) { + return .image + } + else if fileType.isSubtype(of: .text) { + return .text + } + } + return .unknown + } } extension PreviewFileInfo: Equatable { diff --git a/Hotline/Shared/AnimatedImageView.swift b/Hotline/Shared/AnimatedImageView.swift new file mode 100644 index 0000000..0d92715 --- /dev/null +++ b/Hotline/Shared/AnimatedImageView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct AnimatedImageView: NSViewRepresentable { + var image: NSImage? + + let minimumSize: CGSize = CGSize(width: 350, height: 350) + let presentationPaddingRatio: Double = 0.5 + + func makeNSView(context: Context) -> NSImageView { + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.animates = true + imageView.isEditable = false + imageView.allowsCutCopyPaste = true + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + if let img = self.image { + imageView.image = img + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.resizeWindowForImageView(imageView) + } + } + + return imageView + } + + func updateNSView(_ nsView: NSImageView, context: Context) { + nsView.image = self.image + } + + // MARK: - + + func resizeWindowForImageView(_ imageView: NSImageView) { + guard let window = imageView.window, let img = imageView.image else { + return + } + + var windowRect = window.contentLayoutRect + let windowChromeSize = CGSize(width: window.frame.width - windowRect.width, height: window.frame.height - windowRect.height) + + var windowMinSize: CGSize = windowRect.size + let centerPoint = CGPoint(x: window.frame.midX, y: window.frame.midY) + + windowRect.size = img.size + + if let screen = window.screen { + var paddedScreenSize = screen.frame.size + paddedScreenSize.width *= self.presentationPaddingRatio + paddedScreenSize.height *= self.presentationPaddingRatio + + windowMinSize = aspectFit(source: windowRect.size, bounds: self.minimumSize) + windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: windowMinSize) + } + + windowRect.size.width += windowChromeSize.width + windowRect.size.height += windowChromeSize.height + + windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 + windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 + + window.setFrame(windowRect, display: true, animate: true) + +// Do these APIs even work?? +// window.aspectRatio = windowRect.size +// window.contentAspectRatio = windowRect.size + } + + func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { + let sourceAspectRatio = sourceSize.width / sourceSize.height + + var fitSize: CGSize = sourceSize + + if fitSize.width > boundingSize.width { + fitSize.width = boundingSize.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height > boundingSize.height { + fitSize.height = boundingSize.height + fitSize.width = fitSize.height * sourceAspectRatio + } + + if let m = minSize { + if fitSize.width < m.width { + fitSize.width = m.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height < m.height { + fitSize.height = m.height + fitSize.width = fitSize.height * sourceAspectRatio + } + } + + return fitSize + } +} diff --git a/Hotline/Shared/DataAdditions.swift b/Hotline/Shared/DataAdditions.swift new file mode 100644 index 0000000..0e9dd96 --- /dev/null +++ b/Hotline/Shared/DataAdditions.swift @@ -0,0 +1,23 @@ +import Foundation + +extension Data { + func saveAsFileToDownloads(filename: String, bounceDock: Bool = true) -> Bool { + let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let filePath = folderURL.generateUniqueFilePath(filename: filename) + if FileManager.default.createFile(atPath: filePath, contents: nil) { + if let h = FileHandle(forWritingAtPath: filePath) { + try? h.write(contentsOf: self) + try? h.close() + if bounceDock { + #if os(macOS) + var downloadURL = URL(filePath: filePath) + downloadURL.resolveSymlinksInPath() + DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path) + #endif + } + return true + } + } + return false + } +} diff --git a/Hotline/Shared/FileIconView.swift b/Hotline/Shared/FileIconView.swift new file mode 100644 index 0000000..7336cf3 --- /dev/null +++ b/Hotline/Shared/FileIconView.swift @@ -0,0 +1,42 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct FileIconView: View { + let filename: String + + #if os(iOS) + private func fileIcon(filename: String) -> Image { + let fileExtension = (filename as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + if fileType.isSubtype(of: .movie) { + return Image(systemName: "play.rectangle") + } + else if fileType.isSubtype(of: .image) { + return Image(systemName: "photo") + } + else if fileType.isSubtype(of: .archive) { + return Image(systemName: "doc.zipper") + } + else if fileType.isSubtype(of: .text) { + return Image(systemName: "doc.text") + } + else { + return Image(systemName: "doc") + } + } + + 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)) + } + #endif + + + var body: some View { + fileIcon(filename: filename) + .resizable() + .scaledToFit() + } +} diff --git a/Hotline/Shared/URLAdditions.swift b/Hotline/Shared/URLAdditions.swift new file mode 100644 index 0000000..7eebb46 --- /dev/null +++ b/Hotline/Shared/URLAdditions.swift @@ -0,0 +1,42 @@ +import Foundation + +extension URL { + func generateUniqueFilePath(filename base: String) -> String { + let fileManager = FileManager.default + var finalName = base + var counter = 2 + + // Helper function to generate a new filename with a counter + func makeFileName() -> String { + let baseName = (base as NSString).deletingPathExtension + let extensionName = (base as NSString).pathExtension + return extensionName.isEmpty ? "\(baseName) \(counter)" : "\(baseName) \(counter).\(extensionName)" + } + + // Check if file exists and append counter until a unique name is found + var filePath = self.appending(component: finalName).path(percentEncoded: false) + while fileManager.fileExists(atPath: filePath) { + finalName = makeFileName() + filePath = self.appending(component: finalName).path(percentEncoded: false) + counter += 1 + } + + return filePath + } + +// private func prepareDownloadFile(name: String) -> Bool { +// let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] +// let filePath = findUniqueFilePath(base: name, at: folderURL) +// +// if FileManager.default.createFile(atPath: filePath, contents: nil) { +// if let h = FileHandle(forWritingAtPath: filePath) { +// self.filePath = filePath +// self.fileHandle = h +// self.fileProgress?.fileURL = URL(filePath: filePath).resolvingSymlinksInPath() +// return true +// } +// } +// +// return false +// } +} diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift new file mode 100644 index 0000000..1ee4e9b --- /dev/null +++ b/Hotline/macOS/FilePreviewImageView.swift @@ -0,0 +1,124 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct FilePreviewImageView: 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: FilePreview? = nil + @Namespace var mainNamespace + @FocusState private var focusField: FilePreviewFocus? + + var body: some View { + Group { + if preview?.state != .loaded { + HStack(alignment: .center, spacing: 0) { + ProgressView(value: max(0.0, min(1.0, preview?.progress ?? 0.0))) + .focusable(false) + .progressViewStyle(.circular) + .controlSize(.extraLarge) + .tint(.white) + .frame(maxWidth: 300, alignment: .center) + } + .frame(minWidth: 350, maxWidth: 350, minHeight: 150, maxHeight: 150) + .padding() + } + else { + if let image = preview?.image { + AnimatedImageView(image: image) + .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, 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() + } + .frame(minWidth: 350, maxWidth: 350, minHeight: 150, maxHeight: 150) + .padding() + } + } + } + .focusable() + .focusEffectDisabled() + .focused($focusField, equals: .window) + .preferredColorScheme(.dark) + .navigationTitle(info?.name ?? "Preview") + .background(.black) + .toolbar { + ToolbarItem(placement: .navigation) { + FileIconView(filename: info?.name ?? "") + .frame(width: 16, height: 16) + .opacity(controlActiveState == .inactive ? 0.5 : 1.0) + } + + if let img = preview?.image { + if let info = info { + ToolbarItem(placement: .primaryAction) { + Button { + let _ = preview?.data?.saveAsFileToDownloads(filename: info.name) + } label: { + Label("Save Image...", systemImage: "photo.badge.arrow.down") + } + .help("Save Image") + } + + ToolbarItem(placement: .primaryAction) { + ShareLink(item: img, preview: SharePreview(info.name, image: img)) { + Label("Share Image...", systemImage: "square.and.arrow.up") + } + .help("Share Image") + } + } + } + } + .task { + if let info = info { + preview = FilePreview(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() + } + } + } +} diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift new file mode 100644 index 0000000..4c81e6f --- /dev/null +++ b/Hotline/macOS/FilePreviewTextView.swift @@ -0,0 +1,128 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct FilePreviewTextView: 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: FilePreview? = nil + @Namespace var mainNamespace + @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) + .tint(.white) + .frame(maxWidth: 300, alignment: .center) + Spacer() + Spacer() + } + .background(Color(nsColor: .textBackgroundColor)) + .frame(minWidth: 350, maxWidth: .infinity, minHeight: 150, maxHeight: .infinity) + .padding() + } + else { + if let text = preview?.text { + TextEditor(text: .constant(text)) + .textEditorStyle(.plain) + .font(.system(size: 14)) + .lineSpacing(3) + .padding(16) + .contentMargins(.top, -16.0, for: .scrollIndicators) + .contentMargins(.bottom, -16.0, for: .scrollIndicators) + .contentMargins(.trailing, -16.0, for: .scrollIndicators) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .scrollClipDisabled() + } + 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") + .toolbar { + ToolbarItem(placement: .navigation) { + FileIconView(filename: info?.name ?? "") + .frame(width: 16, height: 16) + .opacity(controlActiveState == .inactive ? 0.5 : 1.0) + } + + if let _ = preview?.text { + if let info = info { + ToolbarItem(placement: .primaryAction) { + Button { + let _ = preview?.data?.saveAsFileToDownloads(filename: info.name) + } label: { + Label("Save Text File...", systemImage: "square.and.arrow.down") + } + .help("Save Text File") + } + } + } + } + .task { + if let info = info { + preview = FilePreview(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() + } + } + } +} diff --git a/Hotline/macOS/FilePreviewView.swift b/Hotline/macOS/FilePreviewView.swift deleted file mode 100644 index d170dd7..0000000 --- a/Hotline/macOS/FilePreviewView.swift +++ /dev/null @@ -1,333 +0,0 @@ -import SwiftUI - -class FilePreviewWindowController: NSWindowController, NSWindowDelegate { - init(info: PreviewFileInfo) { - let window = PreviewWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), - styleMask: [.titled, .unifiedTitleAndToolbar, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, defer: false) - - super.init(window: window) - - window.toolbarStyle = .unified - window.delegate = self - window.title = info.name - window.acceptsMouseMovedEvents = true - window.appearance = NSAppearance(named: .darkAqua) - window.collectionBehavior = .fullScreenAuxiliary - - window.standardWindowButton(.closeButton)?.isHidden = false - window.standardWindowButton(.miniaturizeButton)?.isHidden = false - window.standardWindowButton(.zoomButton)?.isHidden = false - - window.center() - - let rootView = PreviewWindowView(frame: NSRect(x: 0, y: 0, width: 480, height: 300)) - rootView.autoresizingMask = [.width, .height] - - let hostingView = NSHostingView(rootView: FilePreviewView(info: info)) - hostingView.frame = rootView.bounds - hostingView.autoresizingMask = [.width, .height] - rootView.addSubview(hostingView) - - window.contentView = rootView - -// let toolbar = NSToolbar() -// toolbar.allowsUserCustomization = false -// toolbar.displayMode = .iconOnly -// -// window.toolbar = toolbar - - self.showWindow(nil) - window.makeFirstResponder(nil) - } - - deinit { - print("FilePreviewWindowController: dealloc") - } - - required init?(coder: NSCoder) { - return nil - } -} - -struct FilePreviewToolbar: View { - var body: some View { -// ToolbarItem(placement: .primaryAction) { - Button { - - } label: { - Image(systemName: "plus") - } - .buttonStyle(.accessoryBar) -// } - } -} - -struct FilePreviewView: View { - enum FilePreviewFocus: Hashable { - case window - } - - @Environment(\.controlActiveState) private var controlActiveState - @Environment(\.colorScheme) private var colorScheme - - let info: PreviewFileInfo - - @State var preview: FilePreview? = nil - @Namespace var mainNamespace - @FocusState private var focusField: FilePreviewFocus? - - var body: some View { - Group { - if preview?.state != .loaded { - ProgressView(value: max(0.0, min(1.0, preview?.progress ?? 0.0))) - .focusable(false) - .padding() - .accentColor(colorScheme == .dark ? .white : .black) - .frame(maxWidth: 250) - .focusEffectDisabled() - } - else { - if let img = preview?.image { - AnimatedImageView(image: img) - .focusable(false) - .scaledToFit() - .focusEffectDisabled() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - else { - Text("UNSUPPORTED FORMAT:") - } - } - } - .focusable() - .focusEffectDisabled() - .focused($focusField, equals: .window) -// .prefersDefaultFocus(in: mainNamespace) -// .focusScope(mainNamespace) -// .focusSection() - - .ignoresSafeArea() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.black) - .overlay(DraggableWindowView()) - .task { - preview = FilePreview(info: info) - preview?.download() - } - .onAppear { - focusField = .window - } - .onDisappear { - preview?.cancel() - } - } -} - -fileprivate struct AnimatedImageView: NSViewRepresentable { - var image: NSImage? - - func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { - let sourceAspectRatio = sourceSize.width / sourceSize.height - - var fitSize: CGSize = sourceSize - - if fitSize.width > boundingSize.width { - fitSize.width = boundingSize.width - fitSize.height = fitSize.width / sourceAspectRatio - } - - if fitSize.height > boundingSize.height { - fitSize.height = boundingSize.height - fitSize.width = fitSize.height * sourceAspectRatio - } - - if let m = minSize { - if fitSize.width < m.width { - fitSize.width = m.width - fitSize.height = fitSize.width / sourceAspectRatio - } - - if fitSize.height < m.height { - fitSize.height = m.height - fitSize.width = fitSize.height * sourceAspectRatio - } - } - - return fitSize - } - - func makeNSView(context: Context) -> NSImageView { - let imageView = NSImageView() - imageView.imageScaling = .scaleProportionallyUpOrDown - imageView.animates = true - imageView.isEditable = false - imageView.allowsCutCopyPaste = true - imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - if let img = self.image { - imageView.image = img - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if let window = imageView.window { - window.aspectRatio = img.size - - var windowRect = window.frame - let centerPoint = CGPoint(x: windowRect.midX, y: windowRect.midY) - - windowRect.size = img.size - - if let screen = window.screen { - var paddedScreenSize = screen.frame.size - paddedScreenSize.width *= 0.5 - paddedScreenSize.height *= 0.5 - - window.minSize = aspectFit(source: windowRect.size, bounds: CGSize(width: 400, height: 400)) - windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: window.minSize) - } - - windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 - windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 - - window.setFrame(windowRect, display: true, animate: true) - } - } - } - - return imageView - } - - func updateNSView(_ nsView: NSImageView, context: Context) { - nsView.image = self.image - } -} - -fileprivate struct DraggableWindowView: NSViewRepresentable { - func makeNSView(context: Context) -> NSView { - return DraggableWindowNSView() - } - - func updateNSView(_ nsView: NSView, context: Context) { } -} - -fileprivate class DraggableWindowNSView: NSView { - override public func mouseDown(with event: NSEvent) { - window?.performDrag(with: event) - } -} - -fileprivate class PreviewWindow: NSWindow { - override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { - super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) - self.minSize = NSSize(width: 200, height: 200) - self.isReleasedWhenClosed = true - self.titlebarSeparatorStyle = .line - self.acceptsMouseMovedEvents = true - } - - deinit { - print("FilePreviewWindow: dealloc") - } - - override var canBecomeMain: Bool { return true } - override var canBecomeKey: Bool { return true } -} - -fileprivate class PreviewWindowView: NSView { - private var trackingArea: NSTrackingArea? = nil - private var windowIsFullscreen: Bool = false - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - self.updateTrackingAreas() - } - - required init?(coder: NSCoder) { - return nil - } - - deinit { - print("FilePreviewWindowView: dealloc") - } - - override func updateTrackingAreas() { - if let t = self.trackingArea { - self.removeTrackingArea(t) - self.trackingArea = nil - } - - let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] - self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) - self.addTrackingArea(self.trackingArea!) - } - -// override func viewDidMoveToWindow() { -// print("VIEW MOVED TO WINDOW?") -// if let w = self.window { -//// w.titlebarAppearsTransparent = false -// let toolbarButtons = NSHostingView(rootView: FilePreviewToolbar()) -// toolbarButtons.frame.size = toolbarButtons.fittingSize -// -// let titlebarAccessory = NSTitlebarAccessoryViewController() -// titlebarAccessory.automaticallyAdjustsSize = true -// titlebarAccessory.view = toolbarButtons -// titlebarAccessory.layoutAttribute = .trailing -// w.addTitlebarAccessoryViewController(titlebarAccessory) -// } -// } - - override func mouseEntered(with event: NSEvent) { - if self.windowIsFullscreen { - return - } - - if let w = self.window { - w.titlebarAppearsTransparent = false - w.titleVisibility = .visible - w.standardWindowButton(.closeButton)?.isHidden = false - w.standardWindowButton(.miniaturizeButton)?.isHidden = false - w.standardWindowButton(.zoomButton)?.isHidden = false - w.toolbar?.isVisible = true - } - } - - override func mouseExited(with event: NSEvent) { - if self.windowIsFullscreen { - return - } - - if let w = self.window { - w.titlebarAppearsTransparent = true - w.titleVisibility = .hidden - w.standardWindowButton(.closeButton)?.isHidden = true - w.standardWindowButton(.miniaturizeButton)?.isHidden = true - w.standardWindowButton(.zoomButton)?.isHidden = true - w.toolbar?.isVisible = false - } - } - - func setWindowIsFullscreen(_ isFullscreen: Bool) { - self.windowIsFullscreen = isFullscreen - - if let w = self.window, isFullscreen { - w.titlebarAppearsTransparent = false - w.titleVisibility = .visible - w.standardWindowButton(.closeButton)?.isHidden = false - w.standardWindowButton(.miniaturizeButton)?.isHidden = false - w.standardWindowButton(.zoomButton)?.isHidden = false - } - } - - override var acceptsFirstResponder: Bool { return true } - - override func keyDown(with event: NSEvent) { - switch event.charactersIgnoringModifiers?.first { - case " ": - self.window?.performClose(nil) - default: - break - } - } -} diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift index 5443270..1eab0cf 100644 --- a/Hotline/macOS/FilesView.swift +++ b/Hotline/macOS/FilesView.swift @@ -41,9 +41,9 @@ struct FileView: View { Image(systemName: "folder.fill") } else { - fileIcon(name: file.name) - .resizable() - .aspectRatio(contentMode: .fit) + FileIconView(filename: file.name) +// .resizable() +// .aspectRatio(contentMode: .fit) // .scaledToFill() .frame(width: 16, height: 16) } @@ -98,11 +98,6 @@ struct FileView: View { FileView.byteFormatter.countStyle = .file return FileView.byteFormatter.string(fromByteCount: Int64(fileSize)) } - - private func fileIcon(name: String) -> Image { - let fileExtension = (name as NSString).pathExtension - return Image(nsImage: NSWorkspace.shared.icon(for: UTType(filenameExtension: fileExtension) ?? UTType.content)) - } } struct FilesView: View { @@ -112,7 +107,15 @@ struct FilesView: View { @State private var selection: FileInfo? private func openPreviewWindow(_ previewInfo: PreviewFileInfo) { - let _ = FilePreviewWindowController(info: previewInfo) + switch previewInfo.previewType { + case .image: + openWindow(id: "preview-image", value: previewInfo) + case .text: + openWindow(id: "preview-text", value: previewInfo) + default: + return + } +// let _ = FilePreviewWindowController(info: previewInfo) } var body: some View { @@ -160,7 +163,7 @@ struct FilesView: View { return .ignored } .onKeyPress(.space) { - if let s = selection, s.isImage { + if let s = selection, s.isPreviewable { model.previewFile(s.name, path: s.path) { info in if let info = info { openPreviewWindow(info) @@ -201,7 +204,7 @@ struct FilesView: View { Label("Preview File", systemImage: "eye") } .help("Preview File") - .disabled(selection?.isImage == false) + .disabled(selection?.isPreviewable == false) } ToolbarItem(placement: .primaryAction) { diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index ea0a090..366fff2 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -52,12 +52,7 @@ struct ListItemView: View { struct TransferItemView: View { let transfer: TransferInfo - - private func fileIcon(name: String) -> Image { - let fileExtension = (name as NSString).pathExtension - return Image(nsImage: NSWorkspace.shared.icon(for: UTType(filenameExtension: fileExtension) ?? UTType.content)) - } - + @Environment(Hotline.self) private var model: Hotline @State private var hovered: Bool = false @State private var buttonHovered: Bool = false @@ -84,9 +79,7 @@ struct TransferItemView: View { HStack(alignment: .center) { HStack(spacing: 0) { Spacer() - fileIcon(name: transfer.title) - .resizable() - .aspectRatio(contentMode: .fit) + FileIconView(filename: transfer.title) .frame(width: 16, height: 16) Spacer() } |