aboutsummaryrefslogtreecommitdiff
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
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.
-rw-r--r--Hotline.xcodeproj/project.pbxproj36
-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
15 files changed, 616 insertions, 406 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index f46c24f..0574111 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -32,7 +32,12 @@
DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFCA2B126E3300CDA197 /* HotlineTrackerClient.swift */; };
DAAEE66B2B3FBC2100A5BA07 /* TransferInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66A2B3FBC2100A5BA07 /* TransferInfo.swift */; };
DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */; };
- DAAEE66F2B47625600A5BA07 /* FilePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66E2B47625600A5BA07 /* FilePreviewView.swift */; platformFilters = (macos, ); };
+ DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */; platformFilters = (macos, ); };
+ DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */; platformFilters = (macos, ); };
+ DAB4D87E2B4C8BCA0048A05C /* FilePreviewTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */; platformFilters = (macos, ); };
+ DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */; };
+ DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D8812B4C8FED0048A05C /* FileIconView.swift */; };
+ DAB4D8842B4CABEF0048A05C /* DataAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */; };
DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */; };
DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */; };
DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28A2B22B31F0024040D /* Tracker.swift */; };
@@ -78,7 +83,12 @@
DA9CAFCA2B126E3300CDA197 /* HotlineTrackerClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineTrackerClient.swift; sourceTree = "<group>"; };
DAAEE66A2B3FBC2100A5BA07 /* TransferInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferInfo.swift; sourceTree = "<group>"; };
DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewFileInfo.swift; sourceTree = "<group>"; };
- DAAEE66E2B47625600A5BA07 /* FilePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewView.swift; sourceTree = "<group>"; };
+ DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewImageView.swift; sourceTree = "<group>"; };
+ DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
+ DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewTextView.swift; sourceTree = "<group>"; };
+ DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLAdditions.swift; sourceTree = "<group>"; };
+ DAB4D8812B4C8FED0048A05C /* FileIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIconView.swift; sourceTree = "<group>"; };
+ DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAdditions.swift; sourceTree = "<group>"; };
DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; };
DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = "<group>"; };
DADDB28A2B22B31F0024040D /* Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracker.swift; sourceTree = "<group>"; };
@@ -144,6 +154,7 @@
DAE734F72B2E4126000C56F6 /* macOS */,
DADDB2892B22B2C60024040D /* Models */,
DABFCC262B1530AE009F40D2 /* Hotline */,
+ DAB4D87C2B4B783A0048A05C /* Shared */,
DABFCC272B1530BE009F40D2 /* Utility */,
DA9CAFC02B126D5800CDA197 /* Assets.xcassets */,
DA32CD472B28EE640053B98B /* Info.plist */,
@@ -161,6 +172,17 @@
path = "Preview Content";
sourceTree = "<group>";
};
+ DAB4D87C2B4B783A0048A05C /* Shared */ = {
+ isa = PBXGroup;
+ children = (
+ DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */,
+ DAB4D8812B4C8FED0048A05C /* FileIconView.swift */,
+ DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */,
+ DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */,
+ );
+ path = Shared;
+ sourceTree = "<group>";
+ };
DABFCC262B1530AE009F40D2 /* Hotline */ = {
isa = PBXGroup;
children = (
@@ -213,7 +235,8 @@
DAE735022B30C0BB000C56F6 /* MessageView.swift */,
DAE734FC2B2E65E9000C56F6 /* MessageBoardView.swift */,
DAE735002B2E71F2000C56F6 /* FilesView.swift */,
- DAAEE66E2B47625600A5BA07 /* FilePreviewView.swift */,
+ DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */,
+ DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */,
DA2863D72B37AD1C00A7D050 /* SettingsView.swift */,
);
path = macOS;
@@ -294,6 +317,7 @@
DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */,
DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */,
DAE735092B329810000C56F6 /* VisualEffectView.swift in Sources */,
+ DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */,
DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */,
DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */,
DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */,
@@ -303,6 +327,7 @@
DADDB28D2B22B5920024040D /* Server.swift in Sources */,
DAE734FB2B2E41F9000C56F6 /* TrackerView.swift in Sources */,
DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */,
+ DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */,
DAE735032B30C0BB000C56F6 /* MessageView.swift in Sources */,
DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */,
DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */,
@@ -318,13 +343,16 @@
DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */,
DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */,
DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */,
- DAAEE66F2B47625600A5BA07 /* FilePreviewView.swift in Sources */,
+ DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */,
+ DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */,
DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */,
+ DAB4D8842B4CABEF0048A05C /* DataAdditions.swift in Sources */,
DA0D698D2B1E7CF700C71DF5 /* UsersView.swift in Sources */,
DAE735052B3218D8000C56F6 /* NewsView.swift in Sources */,
DAE734F92B2E4185000C56F6 /* ServerView.swift in Sources */,
DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */,
DAE734FD2B2E65E9000C56F6 /* MessageBoardView.swift in Sources */,
+ DAB4D87E2B4C8BCA0048A05C /* FilePreviewTextView.swift in Sources */,
DA4F2C012B1A558E00D8ADDC /* ChatView.swift in Sources */,
DA32CD492B2931640053B98B /* User.swift in Sources */,
);
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()
}