فهرست منبع

Add YouTube desktop widgets: medium search, large nav + search

Introduce YoutubeWidgetMode (searchBar, interactiveNav) and
YouTubeDesktopWidgetView with dark YouTube styling. Medium shows search
and open-in-browser; large adds Home, Shorts, Subscriptions, Trending,
History, Library plus the same search row. All targets open in the
in-app browser via WidgetDeepLinkURL / results search_query.

Wire DesktopWidgetView, layout panel sizes, preview card sizes, and
key-capable panels for TextField focus.

Made-with: Cursor
huzaifahayat12 3 ماه پیش
والد
کامیت
6bffaf8140

+ 3 - 1
google_apps/Widgets/DesktopWidgetView.swift

@@ -33,6 +33,8 @@ struct DesktopWidgetView: View {
                 BooksDesktopWidgetView(app: app, mode: booksMode, isPreview: isPreview)
             } else if case .travel(let travelMode) = layoutMode {
                 TravelDesktopWidgetView(app: app, mode: travelMode, isPreview: isPreview)
+            } else if case .youtube(let youtubeMode) = layoutMode {
+                YouTubeDesktopWidgetView(app: app, mode: youtubeMode, isPreview: isPreview)
             } else if case .interactiveMap = layoutMode {
                 MapsInteractiveWidgetView(app: app, widgetSize: size, isPreview: isPreview)
             } else {
@@ -68,7 +70,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel:
+                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .youtube:
                             EmptyView()
                         }
                     }

+ 21 - 14
google_apps/Widgets/WidgetTemplates.swift

@@ -73,6 +73,13 @@ enum TravelWidgetMode: Hashable, Sendable {
     case interactive
 }
 
+enum YoutubeWidgetMode: Hashable, Sendable {
+    /// Search field opens YouTube results in the in-app browser.
+    case searchBar
+    /// Home, Shorts, Subscriptions, and extra feed shortcuts in the in-app browser.
+    case interactiveNav
+}
+
 enum WidgetLayoutMode: Hashable {
     case iconOnly(title: String)
     case actionsRow
@@ -97,6 +104,8 @@ enum WidgetLayoutMode: Hashable {
     case books(BooksWidgetMode)
     /// Google Travel: search, toolbar, and category shortcuts; full site in browser (no trip panel).
     case travel(TravelWidgetMode)
+    /// YouTube: medium search bar or large navigation shortcuts; opens in-app browser.
+    case youtube(YoutubeWidgetMode)
 }
 
 struct WidgetVariant: Identifiable, Hashable {
@@ -242,20 +251,8 @@ enum WidgetTemplates {
                     a("shorts", "Shorts", "bolt", "/shorts"),
                     a("subs", "Subs", "play.rectangle", "/feed/subscriptions"),
                 ]),
-                v("youtube_medium", "Browse", .medium, true, .actionsGrid(columns: 2), [
-                    a("home", "Home", "house", "/"),
-                    a("trending", "Trending", "flame", "/feed/trending"),
-                    a("shorts", "Shorts", "bolt", "/shorts"),
-                    a("subs", "Subscriptions", "play.rectangle", "/feed/subscriptions"),
-                ]),
-                v("youtube_large", "Library", .large, true, .actionsGrid(columns: 2), [
-                    a("home", "Home", "house", "/"),
-                    a("shorts", "Shorts", "play.square", "/shorts"),
-                    a("subscriptions", "Subscriptions", "play.rectangle", "/feed/subscriptions"),
-                    a("history", "History", "clock", "/feed/history"),
-                    a("playlists", "Playlists", "list.bullet.rectangle", "/feed/playlists"),
-                    a("liked", "Liked videos", "hand.thumbsup", "/playlist?list=LL"),
-                ]),
+                v("youtube_medium", "Search", .medium, false, .youtube(.searchBar), []),
+                v("youtube_large", "Navigate", .large, false, .youtube(.interactiveNav), []),
             ]
         case .translate:
             return [
@@ -596,6 +593,8 @@ extension WidgetLayoutMode {
         case .googleSearch(.interactive): true
         case .books(.interactive): true
         case .travel(.interactive): true
+        case .youtube(.searchBar): true
+        case .youtube(.interactiveNav): true
         case .calendar: false
         default: false
         }
@@ -636,6 +635,10 @@ extension WidgetLayoutMode {
             return CGSize(width: 420, height: 260)
         case .travel(.interactive):
             return CGSize(width: 520, height: 196)
+        case .youtube(.searchBar):
+            return CGSize(width: 400, height: 118)
+        case .youtube(.interactiveNav):
+            return CGSize(width: 420, height: 292)
         default:
             return nil
         }
@@ -675,6 +678,10 @@ extension WidgetLayoutMode {
             return CGSize(width: 380, height: 240)
         case .travel(.interactive):
             return CGSize(width: 460, height: 178)
+        case .youtube(.searchBar):
+            return CGSize(width: 360, height: 108)
+        case .youtube(.interactiveNav):
+            return CGSize(width: 380, height: 268)
         default:
             return nil
         }

+ 248 - 0
google_apps/Widgets/YouTubeDesktopWidgetView.swift

@@ -0,0 +1,248 @@
+import AppKit
+import SwiftUI
+
+private let youtubePanelBackground = Color(red: 0.07, green: 0.07, blue: 0.08)
+private let youtubeAccent = Color(red: 0.91, green: 0.12, blue: 0.15)
+
+/// Medium: search only. Large: same search row plus Home, Shorts, Subscriptions, and more — all in-app browser.
+struct YouTubeDesktopWidgetView: View {
+    let app: LauncherApp
+    let mode: YoutubeWidgetMode
+    var isPreview: Bool = false
+
+    @State private var searchText: String = ""
+
+    private var baseURL: URL { app.webURL ?? URL(string: "https://www.youtube.com")! }
+
+    var body: some View {
+        Group {
+            switch mode {
+            case .searchBar:
+                searchBarRoot
+            case .interactiveNav:
+                interactiveNavRoot
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    // MARK: - Medium — search
+
+    private var searchBarRoot: some View {
+        Group {
+            if isPreview {
+                youtubePreviewChrome(
+                    title: "YouTube search",
+                    subtitle: "Search videos and channels; results open in the in-app browser."
+                )
+            } else {
+                searchBarLive
+            }
+        }
+    }
+
+    private var searchBarLive: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            HStack(spacing: 8) {
+                AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("YouTube")
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Spacer(minLength: 0)
+                Button(action: { openInAppBrowser(baseURL) }) {
+                    Image(systemName: "arrow.up.right.square")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.9))
+                        .padding(8)
+                        .background(Circle().fill(Color.white.opacity(0.1)))
+                }
+                .buttonStyle(.plain)
+                .help("Open YouTube in browser")
+            }
+
+            youtubeSearchFieldRow
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .background(
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(youtubePanelBackground)
+        )
+        .preferredColorScheme(.dark)
+    }
+
+    private var youtubeSearchFieldRow: some View {
+        HStack(spacing: 8) {
+            TextField("Search videos or channels", text: $searchText)
+                .textFieldStyle(.plain)
+                .font(.system(size: 13, weight: .medium))
+                .padding(.horizontal, 12)
+                .padding(.vertical, 8)
+                .background(
+                    RoundedRectangle(cornerRadius: 12, style: .continuous)
+                        .fill(Color.white.opacity(0.1))
+                )
+                .foregroundStyle(.white.opacity(0.95))
+                .onSubmit { submitSearch() }
+            Button("Search", action: submitSearch)
+                .font(.system(size: 12, weight: .semibold))
+                .buttonStyle(.borderedProminent)
+                .tint(youtubeAccent)
+                .controlSize(.regular)
+        }
+    }
+
+    // MARK: - Large — navigation
+
+    private var interactiveNavRoot: some View {
+        Group {
+            if isPreview {
+                youtubePreviewChrome(
+                    title: "YouTube",
+                    subtitle: "Search, then jump to Home, Shorts, Subscriptions, and more in the in-app browser."
+                )
+            } else {
+                interactiveNavLive
+            }
+        }
+    }
+
+    private var interactiveNavLive: some View {
+        VStack(alignment: .leading, spacing: 12) {
+            HStack(spacing: 8) {
+                AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("YouTube")
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Spacer(minLength: 0)
+                Button(action: { openYouTubePath("/") }) {
+                    Image(systemName: "arrow.clockwise")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.9))
+                        .padding(8)
+                        .background(Circle().fill(Color.white.opacity(0.1)))
+                }
+                .buttonStyle(.plain)
+                .help("Open Home in browser")
+                Button(action: { openInAppBrowser(baseURL) }) {
+                    Image(systemName: "arrow.up.right.square")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.9))
+                        .padding(8)
+                        .background(Circle().fill(Color.white.opacity(0.1)))
+                }
+                .buttonStyle(.plain)
+                .help("Open YouTube in browser")
+            }
+
+            youtubeSearchFieldRow
+
+            Text("Watch")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.45))
+                .textCase(.uppercase)
+
+            HStack(spacing: 6) {
+                youtubeNavPill(title: "Home", systemImage: "house.fill", path: "/")
+                youtubeNavPill(title: "Shorts", systemImage: "bolt.fill", path: "/shorts")
+                youtubeNavPill(title: "Subscriptions", systemImage: "play.rectangle.fill", path: "/feed/subscriptions")
+            }
+
+            Text("More")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.45))
+                .textCase(.uppercase)
+                .padding(.top, 2)
+
+            HStack(spacing: 6) {
+                youtubeNavPill(title: "Trending", systemImage: "flame.fill", path: "/feed/trending")
+                youtubeNavPill(title: "History", systemImage: "clock.arrow.circlepath", path: "/feed/history")
+                youtubeNavPill(title: "Library", systemImage: "books.vertical.fill", path: "/feed/library")
+            }
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .background(
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(youtubePanelBackground)
+        )
+        .preferredColorScheme(.dark)
+    }
+
+    // MARK: - Shared
+
+    private func youtubePreviewChrome(title: String, subtitle: String) -> some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(
+                    LinearGradient(
+                        colors: [youtubeAccent.opacity(0.85), youtubePanelBackground],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+            VStack(spacing: 12) {
+                AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text(title)
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Text(subtitle)
+                    .font(.system(size: 11, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.75))
+                    .multilineTextAlignment(.center)
+                    .padding(.horizontal, 8)
+            }
+            .padding(16)
+        }
+    }
+
+    private func youtubeNavPill(title: String, systemImage: String, path: String) -> some View {
+        Button {
+            openYouTubePath(path)
+        } label: {
+            VStack(spacing: 5) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 18, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.88))
+                Text(title)
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.82))
+                    .lineLimit(1)
+                    .minimumScaleFactor(0.8)
+            }
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 10)
+            .background(
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
+                    .fill(Color.white.opacity(0.08))
+            )
+            .overlay(
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
+                    .strokeBorder(Color.white.opacity(0.06), lineWidth: 1)
+            )
+        }
+        .buttonStyle(.plain)
+    }
+
+    private func submitSearch() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return }
+        NSApp.activate(ignoringOtherApps: true)
+        var c = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
+        c?.path = "/results"
+        c?.queryItems = [URLQueryItem(name: "search_query", value: q)]
+        if let url = c?.url {
+            openInAppBrowser(url)
+        }
+    }
+
+    private func openYouTubePath(_ path: String) {
+        NSApp.activate(ignoringOtherApps: true)
+        let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
+        openInAppBrowser(url)
+    }
+
+    private func openInAppBrowser(_ url: URL) {
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+}