diff options
| -rw-r--r-- | Hotline.xcodeproj/project.pbxproj | 17 | ||||
| -rw-r--r-- | Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 11 | ||||
| -rw-r--r-- | Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift | 9 | ||||
| -rw-r--r-- | Hotline/Library/Extensions.swift | 36 | ||||
| -rw-r--r-- | Hotline/State/HotlineState.swift | 14 | ||||
| -rw-r--r-- | Hotline/macOS/HotlinePanelView.swift | 64 |
6 files changed, 130 insertions, 21 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index 2861aab..cac321c 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ DA501BE72EBE9542001714F8 /* TrackerBookmarkSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA501BE62EBE9542001714F8 /* TrackerBookmarkSheet.swift */; }; DA501BE92EBE9589001714F8 /* TrackerItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA501BE82EBE9589001714F8 /* TrackerItemView.swift */; }; DA501BEB2EBE95B4001714F8 /* TrackerBookmarkServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA501BEA2EBE95B4001714F8 /* TrackerBookmarkServerView.swift */; }; + DA501BEE2EBEC42E001714F8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DA501BED2EBEC42E001714F8 /* Kingfisher */; }; DA52689C2EB0738B00DCB941 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52689B2EB0738B00DCB941 /* GeneralSettingsView.swift */; platformFilters = (macos, ); }; DA52689E2EB073A400DCB941 /* IconSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52689D2EB073A400DCB941 /* IconSettingsView.swift */; platformFilters = (macos, ); }; DA5268A02EB073BC00DCB941 /* SoundSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52689F2EB073BC00DCB941 /* SoundSettingsView.swift */; platformFilters = (macos, ); }; @@ -227,6 +228,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DA501BEE2EBEC42E001714F8 /* Kingfisher in Frameworks */, DACCE5EF2EAC19C9008CDD92 /* SwiftUIIntrospect in Frameworks */, DA72A0E02B4DA8CA00A0F48A /* SplitView in Frameworks */, DAB4D8872B4CB3610048A05C /* MarkdownUI in Frameworks */, @@ -551,6 +553,7 @@ DAB4D8862B4CB3610048A05C /* MarkdownUI */, DA72A0DF2B4DA8CA00A0F48A /* SplitView */, DACCE5EE2EAC19C9008CDD92 /* SwiftUIIntrospect */, + DA501BED2EBEC42E001714F8 /* Kingfisher */, ); productName = Hotline; productReference = DA9CAFB72B126D5700CDA197 /* Hotline.app */; @@ -584,6 +587,7 @@ DAB4D8852B4CB3610048A05C /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */, DACCE5ED2EAC19C9008CDD92 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + DA501BEC2EBEC42E001714F8 /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = DA9CAFB82B126D5700CDA197 /* Products */; projectDirPath = ""; @@ -934,6 +938,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + DA501BEC2EBEC42E001714F8 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.6.1; + }; + }; DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stevengharris/SplitView"; @@ -961,6 +973,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + DA501BED2EBEC42E001714F8 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = DA501BEC2EBEC42E001714F8 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; DA72A0DF2B4DA8CA00A0F48A /* SplitView */ = { isa = XCSwiftPackageProductDependency; package = DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */; diff --git a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f112b32..3084548 100644 --- a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,7 +1,16 @@ { - "originHash" : "26db0d070cfc8e002044ea1380ec5e675c0718ebf212f6e8a57f12cc6c295723", + "originHash" : "590f8ee2c1318c670012f5d30a51419d3387a50648e0dd04f9654da2b1b7cef5", "pins" : [ { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher", + "state" : { + "revision" : "4d75de347da985a70c63af4d799ed482021f6733", + "version" : "8.6.1" + } + }, + { "identity" : "networkimage", "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/NetworkImage", diff --git a/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift b/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift index 63fe099..93a68fc 100644 --- a/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift +++ b/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift @@ -31,14 +31,7 @@ public class HotlineFilePreviewClientNew { self.transferSize = size } - deinit { - // Cleanup in deinit - must be synchronous - if let tempURL = temporaryFileURL { - try? FileManager.default.removeItem(at: tempURL) - } - } - - // MARK: - Public API + // MARK: - /// Download file to temporary location for preview /// - Parameter progressHandler: Optional progress callback diff --git a/Hotline/Library/Extensions.swift b/Hotline/Library/Extensions.swift index 469676c..b6a4b68 100644 --- a/Hotline/Library/Extensions.swift +++ b/Hotline/Library/Extensions.swift @@ -22,6 +22,42 @@ extension Data { } return false } + + enum ImageFormat { + case gif + case jpeg + case png + case webp + case unknown + } + + var detectedImageFormat: ImageFormat { + guard self.count >= 12 else { return .unknown } + + let bytes = [UInt8](self.prefix(12)) + + // GIF: "GIF8" + if bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x38 { + return .gif + } + + // JPEG: FF D8 FF + if bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF { + return .jpeg + } + + // PNG: 89 50 4E 47 0D 0A 1A 0A + if bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47 && bytes[4] == 0x0D && bytes[5] == 0x0A && bytes[6] == 0x1A && bytes[7] == 0x0A { + return .png + } + + // WebP: "RIFF" at 0-3 and "WEBP" at 8-11 + if bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46 && bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50 { + return .webp + } + + return .unknown + } } // MARK: - diff --git a/Hotline/State/HotlineState.swift b/Hotline/State/HotlineState.swift index d5ab438..4b74df7 100644 --- a/Hotline/State/HotlineState.swift +++ b/Hotline/State/HotlineState.swift @@ -206,6 +206,8 @@ class HotlineState: Equatable { var accountsLoaded: Bool = false // Banner + var bannerFileURL: URL? = nil + var bannerImageFormat: Data.ImageFormat = .unknown #if os(macOS) var bannerImage: Image? = nil var bannerColors: ColorArt? = nil @@ -471,8 +473,10 @@ class HotlineState: Equatable { self.bannerDownloadTask?.cancel() self.bannerDownloadTask = nil self.bannerImage = nil + self.bannerImageFormat = .unknown + self.bannerFileURL = nil self.bannerColors = nil - } else if self.bannerDownloadTask != nil || self.bannerImage != nil { + } else if self.bannerDownloadTask != nil || self.bannerFileURL != nil { return } @@ -503,13 +507,16 @@ class HotlineState: Equatable { ) let fileURL = try await previewClient.preview() - defer { - previewClient.cleanup() + + if let oldFileURL = self.bannerFileURL { + try? FileManager.default.removeItem(at: oldFileURL) } guard self.client != nil else { return } let data = try Data(contentsOf: fileURL) + self.bannerImageFormat = data.detectedImageFormat + print("HotlineState: Banner download complete, data size: \(data.count) bytes") #if os(macOS) @@ -525,6 +532,7 @@ class HotlineState: Equatable { } self.bannerImage = Image(uiImage: image) #endif + self.bannerFileURL = fileURL self.bannerImage = blah self.bannerColors = ColorArt.analyze(image: image) diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift index 81c24b7..220ea97 100644 --- a/Hotline/macOS/HotlinePanelView.swift +++ b/Hotline/macOS/HotlinePanelView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Kingfisher struct HotlinePanelView: View { @Environment(\.openWindow) var openWindow @@ -20,19 +21,25 @@ struct HotlinePanelView: View { private var backgroundColor: Color { Color(nsColor: self.activeHotline?.bannerColors?.backgroundColor ?? NSColor.controlBackgroundColor) } + + private var bannerFileURL: URL? { + self.activeHotline?.bannerFileURL + } + + private var bannerIsAnimated: Bool { + self.activeHotline?.bannerImageFormat == .gif + } var body: some View { VStack(spacing: 0) { - self.bannerImage - .interpolation(.high) - .resizable() - .scaledToFill() - .frame(width: 468, height: 60) - .frame(minWidth: 468, maxWidth: 468, minHeight: 60, maxHeight: 60) - .clipped() - .background(.black) - .animation(.default, value: self.bannerImage) + self.bannerView + .id("banner image view") + .animation(.default, value: self.bannerFileURL) + .background { + Color.black + } + HStack(spacing: 12) { Button { if NSEvent.modifierFlags.contains(.option) { @@ -170,6 +177,45 @@ struct HotlinePanelView: View { // .cornerRadius(10.0) // ) } + + private var bannerView: some View { + ZStack { + if self.bannerIsAnimated { + KFAnimatedImage + .url(self.bannerFileURL) + .placeholder { + Image("Default Banner") + } + .cacheMemoryOnly() + .cacheOriginalImage() + .scaledToFill() + .frame(width: 468, height: 60) + .frame(minWidth: 468, maxWidth: 468, minHeight: 60, maxHeight: 60) + .clipped() + .transition(.opacity) + .id("animated banner \(self.bannerFileURL?.absoluteString ?? "")") + } + else { + KFImage + .url(self.bannerFileURL) + .resizable() + .interpolation(.high) + .placeholder { + Image("Default Banner") + } + .cacheMemoryOnly() + .cacheOriginalImage() + .scaledToFill() + .frame(width: 468, height: 60) + .frame(minWidth: 468, maxWidth: 468, minHeight: 60, maxHeight: 60) + .clipped() + .transition(.opacity) + .id("static banner \(self.bannerFileURL?.absoluteString ?? "")") + } + } + .animation(.default, value: self.bannerIsAnimated) + .animation(.default, value: self.bannerFileURL) + } } #Preview { |