Răsfoiți Sursa

Fetch site favicons dynamically for web tiles

Add WebSiteFaviconView with Google favicon service and in-memory cache.
Use remote icons in AppIconView and IconPanel glyphs when webURL is set,
falling back to bundled assets and SF Symbols.

Made-with: Cursor
huzaifahayat12 4 luni în urmă
părinte
comite
4b0cc05c37

+ 8 - 1
google_apps/AppTileView.swift

@@ -288,7 +288,14 @@ private struct AppIconView: View {
                 RoundedRectangle(cornerRadius: 18, style: .continuous)
                     .stroke(Color.white.opacity(0.08), lineWidth: 1)
 
-                if let iconImage = NSImage(named: app.assetIconName) {
+                if let webURL = app.webURL {
+                    WebSiteFaviconView(
+                        webURL: webURL,
+                        size: size,
+                        assetIconName: app.assetIconName,
+                        fallbackSymbolName: app.fallbackSymbolName
+                    )
+                } else if let iconImage = NSImage(named: app.assetIconName) {
                     Image(nsImage: iconImage)
                         .resizable()
                         .scaledToFit()

+ 9 - 1
google_apps/LauncherRootView.swift

@@ -458,7 +458,15 @@ private struct AppIconGlyph: View {
                 .fill(Color.white.opacity(0.08))
                 .frame(width: 48, height: 48)
 
-            if let icon = NSImage(named: app.assetIconName) {
+            if let webURL = app.webURL {
+                WebSiteFaviconView(
+                    webURL: webURL,
+                    size: 32,
+                    assetIconName: app.assetIconName,
+                    fallbackSymbolName: app.fallbackSymbolName,
+                    iconPaddingFactor: 0.08
+                )
+            } else if let icon = NSImage(named: app.assetIconName) {
                 Image(nsImage: icon)
                     .resizable()
                     .scaledToFit()

+ 76 - 0
google_apps/WebSiteFaviconView.swift

@@ -0,0 +1,76 @@
+import AppKit
+import SwiftUI
+
+/// Fetches site icons via Google’s favicon service (domain-based), with in-memory caching.
+actor FaviconImageCache {
+    static let shared = FaviconImageCache()
+
+    private var memory: [String: NSImage] = [:]
+
+    func image(for siteURL: URL) async -> NSImage? {
+        guard let faviconURL = Self.faviconServiceURL(for: siteURL) else { return nil }
+        let key = faviconURL.absoluteString
+        if let cached = memory[key] { return cached }
+        do {
+            var request = URLRequest(url: faviconURL)
+            request.timeoutInterval = 20
+            request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/605.1.15", forHTTPHeaderField: "User-Agent")
+            let (data, response) = try await URLSession.shared.data(for: request)
+            guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { return nil }
+            guard let image = NSImage(data: data), image.isValid else { return nil }
+            memory[key] = image
+            return image
+        } catch {
+            return nil
+        }
+    }
+
+    private static func faviconServiceURL(for siteURL: URL) -> URL? {
+        guard let host = siteURL.host?.lowercased(), !host.isEmpty else { return nil }
+        var components = URLComponents(string: "https://www.google.com/s2/favicons")!
+        components.queryItems = [
+            URLQueryItem(name: "domain", value: host),
+            URLQueryItem(name: "sz", value: "128"),
+        ]
+        return components.url
+    }
+}
+
+/// Shows a remote favicon when `webURL` is set; uses bundled asset then SF Symbol while loading or on failure.
+struct WebSiteFaviconView: View {
+    let webURL: URL
+    let size: CGFloat
+    let assetIconName: String
+    let fallbackSymbolName: String
+    var iconPaddingFactor: CGFloat = 0.12
+
+    @State private var remoteImage: NSImage?
+
+    var body: some View {
+        Group {
+            if let remote = remoteImage {
+                Image(nsImage: remote)
+                    .resizable()
+                    .interpolation(.high)
+                    .scaledToFit()
+                    .padding(size * iconPaddingFactor)
+            } else if let asset = NSImage(named: assetIconName) {
+                Image(nsImage: asset)
+                    .resizable()
+                    .scaledToFit()
+                    .padding(size * iconPaddingFactor)
+            } else {
+                Image(systemName: fallbackSymbolName)
+                    .font(.system(size: size * 0.32, weight: .semibold))
+                    .foregroundStyle(.white)
+            }
+        }
+        .frame(width: size, height: size)
+        .task(id: webURL.absoluteString) {
+            remoteImage = nil
+            if let img = await FaviconImageCache.shared.image(for: webURL) {
+                remoteImage = img
+            }
+        }
+    }
+}