aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-07 17:40:59 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-07 17:40:59 -0800
commit637cd9af54a58db0c5dc2062b6c20291afd31147 (patch)
tree19c0168a9ee0a2bcb158fdae6192aab361921475 /Hotline
parent6afa5551add4541f376867b3d6527df9a0f793f3 (diff)
Add Kingfisher dependency. Add support for animated GIF banners. Add fast image format detection to Data.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift9
-rw-r--r--Hotline/Library/Extensions.swift36
-rw-r--r--Hotline/State/HotlineState.swift14
-rw-r--r--Hotline/macOS/HotlinePanelView.swift64
4 files changed, 103 insertions, 20 deletions
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 {