aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj17
-rw-r--r--Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved11
-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
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 {