| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- 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/")!
- }
- /// Google Photos–style dark shell (deep blue-grey).
- private var shellGradient: LinearGradient {
- LinearGradient(
- colors: [
- Color(red: 0.07, green: 0.10, blue: 0.14),
- Color(red: 0.05, green: 0.07, blue: 0.11),
- ],
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- }
- var body: some View {
- Group {
- if isPreview {
- photosPreviewChrome
- } else {
- ZStack {
- RoundedRectangle(cornerRadius: 22, style: .continuous)
- .fill(shellGradient)
- VStack(spacing: 0) {
- galleryChromeHeader
- headerDivider
- GooglePhotosEmbeddedWebView(startURL: baseURL)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- bottomChromeBar
- }
- }
- }
- }
- .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: 22, style: .continuous)
- .strokeBorder(Color.white.opacity(0.10), lineWidth: 1)
- )
- }
- private var headerDivider: some View {
- Rectangle()
- .fill(
- LinearGradient(
- colors: [Color.white.opacity(0.14), Color.white.opacity(0.05)],
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(height: 1)
- }
- private var galleryChromeHeader: some View {
- HStack(alignment: .center, spacing: 12) {
- ZStack {
- RoundedRectangle(cornerRadius: 10, style: .continuous)
- .fill(
- LinearGradient(
- colors: [Color.white.opacity(0.14), Color.white.opacity(0.06)],
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- )
- .frame(width: 40, height: 40)
- .overlay(
- RoundedRectangle(cornerRadius: 10, style: .continuous)
- .stroke(Color.white.opacity(0.12), lineWidth: 1)
- )
- AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
- }
- VStack(alignment: .leading, spacing: 3) {
- Text(app.name)
- .font(.system(size: 15, weight: .bold, design: .rounded))
- .foregroundStyle(.white.opacity(0.96))
- HStack(spacing: 6) {
- Text("Recent")
- .font(.system(size: 11, weight: .semibold, design: .rounded))
- .foregroundStyle(Color(red: 0.55, green: 0.82, blue: 0.95).opacity(0.95))
- Text("·")
- .foregroundStyle(.white.opacity(0.35))
- Text("Library")
- .font(.system(size: 11, weight: .medium, design: .rounded))
- .foregroundStyle(.white.opacity(0.55))
- }
- }
- Spacer(minLength: 8)
- Button(action: openPhotosHome) {
- HStack(spacing: 5) {
- Text("Open")
- .font(.system(size: 12, weight: .bold, design: .rounded))
- Image(systemName: "arrow.up.right.circle.fill")
- .font(.system(size: 14, weight: .semibold))
- }
- .foregroundStyle(.white.opacity(0.95))
- .padding(.horizontal, 11)
- .padding(.vertical, 7)
- .background(
- Capsule(style: .continuous)
- .fill(
- LinearGradient(
- colors: [Color.white.opacity(0.22), Color.white.opacity(0.10)],
- startPoint: .top,
- endPoint: .bottom
- )
- )
- )
- .overlay(
- Capsule(style: .continuous)
- .stroke(Color.white.opacity(0.18), lineWidth: 1)
- )
- }
- .buttonStyle(.plain)
- }
- .padding(.horizontal, 14)
- .padding(.vertical, 12)
- .background(
- LinearGradient(
- colors: [Color.black.opacity(0.42), Color.black.opacity(0.22)],
- startPoint: .top,
- endPoint: .bottom
- )
- )
- }
- /// Subtle multi-hue strip echoing Google Photos branding.
- private var photosAccentStrip: some View {
- LinearGradient(
- colors: [
- Color(red: 0.26, green: 0.52, blue: 0.96),
- Color(red: 0.18, green: 0.72, blue: 0.45),
- Color(red: 0.98, green: 0.75, blue: 0.18),
- Color(red: 0.92, green: 0.35, blue: 0.25),
- ],
- startPoint: .leading,
- endPoint: .trailing
- )
- .frame(height: 2)
- .opacity(0.85)
- }
- private var bottomChromeBar: some View {
- VStack(spacing: 0) {
- photosAccentStrip
- HStack(spacing: 10) {
- PhotosShortcutChip(title: "Albums", systemImage: "rectangle.stack.fill") {
- openPhotosPath("/albums")
- }
- PhotosShortcutChip(title: "Memories", systemImage: "sparkles") {
- openPhotosPath("/memories")
- }
- PhotosShortcutChip(title: "Search", systemImage: "magnifyingglass") {
- openPhotosPath("/search")
- }
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 12)
- .background(
- LinearGradient(
- colors: [Color.black.opacity(0.55), Color.black.opacity(0.38)],
- startPoint: .top,
- endPoint: .bottom
- )
- )
- }
- }
- private struct PhotosShortcutChip: View {
- let title: String
- let systemImage: String
- let action: () -> Void
- var body: some View {
- Button(action: action) {
- VStack(spacing: 6) {
- Image(systemName: systemImage)
- .font(.system(size: 15, weight: .semibold))
- .foregroundStyle(
- LinearGradient(
- colors: [
- Color.white.opacity(0.95),
- Color.white.opacity(0.72),
- ],
- startPoint: .top,
- endPoint: .bottom
- )
- )
- Text(title)
- .font(.system(size: 10, weight: .bold, design: .rounded))
- .foregroundStyle(.white.opacity(0.88))
- .lineLimit(1)
- .minimumScaleFactor(0.85)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 11)
- .padding(.horizontal, 6)
- .background(
- RoundedRectangle(cornerRadius: 14, style: .continuous)
- .fill(
- LinearGradient(
- colors: [Color.white.opacity(0.14), Color.white.opacity(0.05)],
- startPoint: .top,
- endPoint: .bottom
- )
- )
- )
- .overlay(
- RoundedRectangle(cornerRadius: 14, style: .continuous)
- .stroke(Color.white.opacity(0.14), lineWidth: 1)
- )
- }
- .buttonStyle(.plain)
- }
- }
- private var photosPreviewChrome: some View {
- ZStack {
- RoundedRectangle(cornerRadius: 22, style: .continuous)
- .fill(
- LinearGradient(
- colors: [
- Color(red: 0.10, green: 0.14, blue: 0.20),
- Color(red: 0.04, green: 0.06, blue: 0.10),
- ],
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- )
- VStack(spacing: 14) {
- ZStack {
- Circle()
- .fill(
- LinearGradient(
- colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- )
- .frame(width: 72, height: 72)
- .overlay(Circle().stroke(Color.white.opacity(0.14), lineWidth: 1))
- AppIconView(app: app, size: 44, showAppBackground: false, iconPaddingFactor: 0.08)
- }
- Text("Recent gallery")
- .font(.system(size: 16, weight: .bold, design: .rounded))
- .foregroundStyle(.white.opacity(0.94))
- Text("Your library appears here after you sign in to Google Photos in the panel.")
- .font(.system(size: 11, weight: .medium, design: .rounded))
- .foregroundStyle(.white.opacity(0.62))
- .multilineTextAlignment(.center)
- .lineSpacing(2)
- .padding(.horizontal, 18)
- }
- .padding(20)
- }
- .overlay(
- RoundedRectangle(cornerRadius: 22, style: .continuous)
- .strokeBorder(Color.white.opacity(0.10), lineWidth: 1)
- )
- }
- private func openPhotosHome() {
- NSApp.activate(ignoringOtherApps: true)
- InAppBrowserWindowManager.shared.open(url: baseURL, title: app.name)
- }
- 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)
- }
- }
- }
- }
- }
|