|
|
@@ -0,0 +1,146 @@
|
|
|
+import AppKit
|
|
|
+import SwiftUI
|
|
|
+
|
|
|
+private let contactsAccent = Color(red: 0.26, green: 0.61, blue: 0.95)
|
|
|
+private let contactsPanelBg = Color(red: 0.08, green: 0.12, blue: 0.18)
|
|
|
+
|
|
|
+/// Medium-only: search opens Google Contacts with the query; Frequent contacts + Open App shortcuts (no contact list in-widget).
|
|
|
+struct ContactsDesktopWidgetView: View {
|
|
|
+ let app: LauncherApp
|
|
|
+ let mode: ContactsWidgetMode
|
|
|
+ var isPreview: Bool = false
|
|
|
+
|
|
|
+ @State private var searchText: String = ""
|
|
|
+
|
|
|
+ private var baseURL: URL {
|
|
|
+ app.webURL ?? URL(string: "https://contacts.google.com")!
|
|
|
+ }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Group {
|
|
|
+ switch mode {
|
|
|
+ case .searchHub:
|
|
|
+ searchHubBody
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
+ }
|
|
|
+
|
|
|
+ private var searchHubBody: some View {
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color(red: 0.14, green: 0.22, blue: 0.36), contactsPanelBg],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
|
|
|
+ )
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Google Contacts")
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ Text("Search opens Contacts with your query.")
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ .lineLimit(2)
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: "magnifyingglass")
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
+ TextField("Name or email", text: $searchText)
|
|
|
+ .textFieldStyle(.plain)
|
|
|
+ .font(.system(size: 13, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .onSubmit(submitSearch)
|
|
|
+ Button("Search", action: submitSearch)
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
+ .buttonStyle(.borderedProminent)
|
|
|
+ .tint(contactsAccent)
|
|
|
+ .controlSize(.small)
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 10)
|
|
|
+ .padding(.vertical, 8)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.white.opacity(0.1))
|
|
|
+ )
|
|
|
+
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ shortcutButton(title: "Frequent contacts", systemImage: "clock.arrow.circlepath", action: openFrequentContactsShortcut)
|
|
|
+ shortcutButton(title: "Open App", systemImage: "arrow.up.right.square", action: openContactsApp)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(12)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
+ }
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .opacity(isPreview ? 0.88 : 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func shortcutButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
+ Button(action: action) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.75)
|
|
|
+ }
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 9)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.white.opacity(0.12))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .strokeBorder(Color.white.opacity(0.1), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func submitSearch() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ if q.isEmpty {
|
|
|
+ InAppBrowserWindowManager.shared.open(url: baseURL, title: app.name)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let encoded = q.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? q
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: "/search/\(encoded)", accountSlot: 0)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Frequently contacted — official Contacts path is `/frequent`.
|
|
|
+ private func openFrequentContactsShortcut() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: "/frequent", accountSlot: 0)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openContactsApp() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: baseURL, title: app.name)
|
|
|
+ }
|
|
|
+}
|