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/macOS | |
| 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/macOS')
| -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 |
5 files changed, 268 insertions, 353 deletions
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() } |