aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-01-08 14:45:58 -0800
committerDustin Mierau <dustin@mierau.me>2024-01-08 14:45:58 -0800
commite9bae0328801d5a056f33beb0f764c6d35e518a2 (patch)
treefcf853330f8de68948b089a5b15e2ef4c6dbb47c /Hotline/macOS
parent4349f50167f50b30d7cd71854882ab33ac6ea5ea (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.swift124
-rw-r--r--Hotline/macOS/FilePreviewTextView.swift128
-rw-r--r--Hotline/macOS/FilePreviewView.swift333
-rw-r--r--Hotline/macOS/FilesView.swift25
-rw-r--r--Hotline/macOS/ServerView.swift11
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()
}