|
|
@@ -0,0 +1,196 @@
|
|
|
+import AppKit
|
|
|
+import SwiftUI
|
|
|
+import WebKit
|
|
|
+
|
|
|
+/// Google Photos: medium embedded recent gallery with Albums / Memories / Search shortcuts.
|
|
|
+struct PhotosDesktopWidgetView: View {
|
|
|
+ let app: LauncherApp
|
|
|
+ var isPreview: Bool = false
|
|
|
+
|
|
|
+ private var baseURL: URL {
|
|
|
+ app.webURL ?? URL(string: "https://photos.google.com/")!
|
|
|
+ }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Group {
|
|
|
+ if isPreview {
|
|
|
+ photosPreviewChrome
|
|
|
+ } else {
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ galleryHeader(title: "Recent", subtitle: "photos.google.com")
|
|
|
+ GooglePhotosEmbeddedWebView(startURL: baseURL)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ mediumShortcutRow
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
+ }
|
|
|
+
|
|
|
+ private var photosPreviewChrome: some View {
|
|
|
+ ZStack {
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color(red: 0.12, green: 0.16, blue: 0.22), Color(red: 0.05, green: 0.08, blue: 0.12)],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ VStack(spacing: 12) {
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Recent gallery")
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ Text("Live Google Photos grid in the panel after you sign in.")
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
+ .padding(.horizontal, 12)
|
|
|
+ }
|
|
|
+ .padding(16)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func galleryHeader(title: String, subtitle: String) -> some View {
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
+ Text(app.name)
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 11, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ }
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ Text(subtitle)
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.4))
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 14)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(Color.black.opacity(0.35))
|
|
|
+ }
|
|
|
+
|
|
|
+ private var mediumShortcutRow: some View {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ shortcutPill(title: "Albums", systemImage: "rectangle.stack", path: "/albums")
|
|
|
+ shortcutPill(title: "Memories", systemImage: "sparkles", path: "/memories")
|
|
|
+ shortcutPill(title: "Search", systemImage: "magnifyingglass", path: "/search")
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 12)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(Color.black.opacity(0.4))
|
|
|
+ }
|
|
|
+
|
|
|
+ private func shortcutPill(title: String, systemImage: String, path: String) -> some View {
|
|
|
+ Button(action: { openPhotosPath(path) }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 11, weight: .semibold))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ .padding(.horizontal, 12)
|
|
|
+ .padding(.vertical, 8)
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openPhotosPath(_ path: String) {
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - WKWebView
|
|
|
+
|
|
|
+private final class PhotosWebViewClippingContainer: NSView {
|
|
|
+ override var isFlipped: Bool { true }
|
|
|
+}
|
|
|
+
|
|
|
+private struct GooglePhotosEmbeddedWebView: NSViewRepresentable {
|
|
|
+ let startURL: URL
|
|
|
+
|
|
|
+ func makeCoordinator() -> Coordinator {
|
|
|
+ Coordinator()
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeNSView(context: Context) -> NSView {
|
|
|
+ let container = PhotosWebViewClippingContainer()
|
|
|
+ container.wantsLayer = true
|
|
|
+ container.layer?.masksToBounds = true
|
|
|
+
|
|
|
+ let config = WKWebViewConfiguration()
|
|
|
+ config.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
|
+ let embedded = WKWebView(frame: .zero, configuration: config)
|
|
|
+ embedded.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ embedded.focusRingType = .none
|
|
|
+ embedded.navigationDelegate = context.coordinator
|
|
|
+ context.coordinator.embeddedWebView = embedded
|
|
|
+ context.coordinator.pageURL = startURL
|
|
|
+ context.coordinator.reload()
|
|
|
+
|
|
|
+ container.addSubview(embedded)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ embedded.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
+ embedded.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
+ embedded.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
+ embedded.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
+ ])
|
|
|
+
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ Self.stripWebKitOuterChrome(embedded)
|
|
|
+ }
|
|
|
+ return container
|
|
|
+ }
|
|
|
+
|
|
|
+ func updateNSView(_ container: NSView, context: Context) {
|
|
|
+ guard context.coordinator.pageURL != startURL else { return }
|
|
|
+ context.coordinator.pageURL = startURL
|
|
|
+ context.coordinator.reload()
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func stripWebKitOuterChrome(_ root: WKWebView) {
|
|
|
+ func walk(_ view: NSView) {
|
|
|
+ view.focusRingType = .none
|
|
|
+ view.layer?.borderWidth = 0
|
|
|
+ view.layer?.borderColor = NSColor.clear.cgColor
|
|
|
+ if let scroll = view as? NSScrollView {
|
|
|
+ scroll.borderType = .noBorder
|
|
|
+ scroll.drawsBackground = false
|
|
|
+ scroll.scrollerStyle = .overlay
|
|
|
+ }
|
|
|
+ if let clip = view as? NSClipView {
|
|
|
+ clip.drawsBackground = false
|
|
|
+ }
|
|
|
+ for sub in view.subviews {
|
|
|
+ walk(sub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ walk(root)
|
|
|
+ }
|
|
|
+
|
|
|
+ final class Coordinator: NSObject, WKNavigationDelegate {
|
|
|
+ weak var embeddedWebView: WKWebView?
|
|
|
+ var pageURL = URL(string: "https://photos.google.com/")!
|
|
|
+
|
|
|
+ func reload() {
|
|
|
+ guard let embeddedWebView else { return }
|
|
|
+ embeddedWebView.load(URLRequest(url: pageURL))
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ GooglePhotosEmbeddedWebView.stripWebKitOuterChrome(webView)
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
|
+ GooglePhotosEmbeddedWebView.stripWebKitOuterChrome(webView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|