|
|
@@ -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)
|
|
|
+ }
|
|
|
+}
|