|
@@ -0,0 +1,188 @@
|
|
|
|
|
+import AppKit
|
|
|
|
|
+import SwiftUI
|
|
|
|
|
+
|
|
|
|
|
+/// Large Google Search widget: compact launcher with quick search, voice shortcut, and category chips.
|
|
|
|
|
+/// Results open in the in-app browser (no embedded web view below the toolbar).
|
|
|
|
|
+struct GoogleSearchDesktopWidgetView: View {
|
|
|
|
|
+ let app: LauncherApp
|
|
|
|
|
+ let mode: GoogleSearchWidgetMode
|
|
|
|
|
+ var isPreview: Bool = false
|
|
|
|
|
+
|
|
|
|
|
+ @State private var quickQuery = ""
|
|
|
|
|
+
|
|
|
|
|
+ private var baseURL: URL { app.webURL ?? URL(string: "https://www.google.com")! }
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ switch mode {
|
|
|
|
|
+ case .interactive:
|
|
|
|
|
+ interactiveBody
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveBody: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ if isPreview {
|
|
|
|
|
+ searchPreviewChrome
|
|
|
|
|
+ } else {
|
|
|
|
|
+ searchLiveChrome
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var searchPreviewChrome: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(
|
|
|
|
|
+ LinearGradient(
|
|
|
|
|
+ colors: [Color(red: 0.15, green: 0.45, blue: 0.95), Color(red: 0.05, green: 0.15, blue: 0.4)],
|
|
|
|
|
+ startPoint: .topLeading,
|
|
|
|
|
+ endPoint: .bottomTrailing
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ VStack(spacing: 12) {
|
|
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Google Search")
|
|
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ Text("Quick search and shortcuts open results in your browser.")
|
|
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.7))
|
|
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
|
|
+ .padding(.horizontal, 8)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var searchLiveChrome: some View {
|
|
|
|
|
+ searchToolbar
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var searchToolbar: some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ AppIconView(app: app, size: 26, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Google")
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ openInAppBrowser(baseURL)
|
|
|
|
|
+ }) {
|
|
|
|
|
+ Image(systemName: "mic.fill")
|
|
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.9))
|
|
|
|
|
+ .padding(8)
|
|
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .help("Open Google in browser to use voice search")
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ 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.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .help("Open Google in browser")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ TextField("Search", text: $quickQuery)
|
|
|
|
|
+ .textFieldStyle(.plain)
|
|
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
|
|
+ .fill(Color.white.opacity(0.1))
|
|
|
|
|
+ )
|
|
|
|
|
+ .onSubmit { submitQuickSearch() }
|
|
|
|
|
+ Button("Go") {
|
|
|
|
|
+ submitQuickSearch()
|
|
|
|
|
+ }
|
|
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(Color(red: 0.25, green: 0.55, blue: 1))
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ searchChip("Trending", systemImage: "chart.line.uptrend.xyaxis") {
|
|
|
|
|
+ openURL(URL(string: "https://trends.google.com/trending?geo=US")!)
|
|
|
|
|
+ }
|
|
|
|
|
+ searchChip("Images", systemImage: "photo") {
|
|
|
|
|
+ openURL(WidgetDeepLinkURL.resolved(base: baseURL, actionPath: "/imghp", accountSlot: 0))
|
|
|
|
|
+ }
|
|
|
|
|
+ searchChip("Maps", systemImage: "map") {
|
|
|
|
|
+ openURL(URL(string: "https://maps.google.com/maps")!)
|
|
|
|
|
+ }
|
|
|
|
|
+ searchChip("Shopping", systemImage: "bag") {
|
|
|
|
|
+ openURL(WidgetDeepLinkURL.resolved(base: baseURL, actionPath: "/search?tbm=shop", accountSlot: 0))
|
|
|
|
|
+ }
|
|
|
|
|
+ searchChip("News", systemImage: "newspaper") {
|
|
|
|
|
+ openURL(URL(string: "https://news.google.com")!)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 10)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(Color(red: 0.06, green: 0.1, blue: 0.18))
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func searchChip(_ title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ action()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 4) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 11, weight: .semibold))
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ }
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ .contentShape(Capsule())
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func submitQuickSearch() {
|
|
|
|
|
+ let q = quickQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard !q.isEmpty else { return }
|
|
|
|
|
+ navigateToSearch(q)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func navigateToSearch(_ query: String) {
|
|
|
|
|
+ var c = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
|
|
|
|
|
+ c?.path = "/search"
|
|
|
|
|
+ c?.queryItems = [URLQueryItem(name: "q", value: query)]
|
|
|
|
|
+ guard let u = c?.url else { return }
|
|
|
|
|
+ openInAppBrowser(u)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openURL(_ url: URL) {
|
|
|
|
|
+ openInAppBrowser(url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openInAppBrowser(_ url: URL) {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|