aboutsummaryrefslogtreecommitdiff
path: root/Hotline
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
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')
-rw-r--r--Hotline/Application.swift33
-rw-r--r--Hotline/Hotline/HotlineFileClient.swift39
-rw-r--r--Hotline/Models/FileInfo.swift13
-rw-r--r--Hotline/Models/FilePreview.swift55
-rw-r--r--Hotline/Models/PreviewFileInfo.swift20
-rw-r--r--Hotline/Shared/AnimatedImageView.swift98
-rw-r--r--Hotline/Shared/DataAdditions.swift23
-rw-r--r--Hotline/Shared/FileIconView.swift42
-rw-r--r--Hotline/Shared/URLAdditions.swift42
-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
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()
}