|
|
@@ -0,0 +1,596 @@
|
|
|
+import AppKit
|
|
|
+import SwiftUI
|
|
|
+import WebKit
|
|
|
+
|
|
|
+// MARK: - Public SwiftUI view
|
|
|
+
|
|
|
+/// Tiered Gmail desktop widget: compact unread + compose, inbox preview, or full embedded Gmail.
|
|
|
+struct GmailDesktopWidgetView: View {
|
|
|
+ let app: LauncherApp
|
|
|
+ let mode: GmailWidgetMode
|
|
|
+ var gmailAccountSlot: Int = 0
|
|
|
+ var isPreview: Bool = false
|
|
|
+
|
|
|
+ private var baseURL: URL { app.webURL ?? URL(string: "https://mail.google.com")! }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Group {
|
|
|
+ switch mode {
|
|
|
+ case .compact:
|
|
|
+ compactBody
|
|
|
+ case .inboxPreview:
|
|
|
+ inboxPreviewBody
|
|
|
+ case .interactive:
|
|
|
+ interactiveBody
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Compact (small)
|
|
|
+
|
|
|
+ @AppStorage("gmailDesktopWidgetUnreadCount") private var unreadCountStored: Int = -1
|
|
|
+
|
|
|
+ private var compactBody: some View {
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
+ gmailGradientBackground
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
+ Text(app.name)
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Text("Unread")
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ }
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ HStack(alignment: .firstTextBaseline, spacing: 6) {
|
|
|
+ Text(unreadCountStored < 0 ? "—" : "\(unreadCountStored)")
|
|
|
+ .font(.system(size: 36, weight: .bold, design: .rounded))
|
|
|
+ .foregroundStyle(.white)
|
|
|
+ .minimumScaleFactor(0.5)
|
|
|
+ .lineLimit(1)
|
|
|
+ Text("messages")
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.7))
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ Text("Local count only — use Open Mail for your live inbox.")
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.52))
|
|
|
+ .lineLimit(2)
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+
|
|
|
+ compactButton(title: "Open Mail", systemImage: "envelope.open", emphasized: false) {
|
|
|
+ openMailInBrowser()
|
|
|
+ }
|
|
|
+
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ compactButton(title: "Compose", systemImage: "square.and.pencil", emphasized: true) {
|
|
|
+ openCompose()
|
|
|
+ }
|
|
|
+ compactButton(title: "Unread", systemImage: "envelope.badge", emphasized: false) {
|
|
|
+ openUnreadSearch()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(14)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .opacity(isPreview ? 0.85 : 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Medium (inbox preview)
|
|
|
+
|
|
|
+ private struct PreviewRow: Identifiable {
|
|
|
+ let id: String
|
|
|
+ let sender: String
|
|
|
+ let subject: String
|
|
|
+ let time: String
|
|
|
+ let searchQuery: String
|
|
|
+ }
|
|
|
+
|
|
|
+ private static let previewRows: [PreviewRow] = [
|
|
|
+ PreviewRow(id: "1", sender: "Alex Chen", subject: "Q4 roadmap — draft review", time: "9:42a", searchQuery: "from:alex"),
|
|
|
+ PreviewRow(id: "2", sender: "Google Alerts", subject: "News: your weekly digest", time: "8:10a", searchQuery: "from:googlealerts"),
|
|
|
+ PreviewRow(id: "3", sender: "HR Benefits", subject: "Open enrollment ends Friday", time: "Yesterday", searchQuery: "from:hr"),
|
|
|
+ ]
|
|
|
+
|
|
|
+ private var inboxPreviewBody: some View {
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
+ gmailGradientBackground
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ ScrollView {
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Inbox preview")
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ Text("Sample")
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ .padding(.horizontal, 8)
|
|
|
+ .padding(.vertical, 4)
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
+ }
|
|
|
+
|
|
|
+ compactButton(title: "Open Mail", systemImage: "envelope.open", emphasized: false) {
|
|
|
+ openMailInBrowser()
|
|
|
+ }
|
|
|
+
|
|
|
+ Text("Recent messages")
|
|
|
+ .font(.system(size: 11, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
+
|
|
|
+ VStack(spacing: 8) {
|
|
|
+ ForEach(Self.previewRows) { row in
|
|
|
+ previewRowView(row)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .padding(.bottom, 4)
|
|
|
+ }
|
|
|
+ .padding(14)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .opacity(isPreview ? 0.85 : 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func previewRowView(_ row: PreviewRow) -> some View {
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
+ HStack(alignment: .top, spacing: 8) {
|
|
|
+ VStack(alignment: .leading, spacing: 3) {
|
|
|
+ Text(row.sender)
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .lineLimit(1)
|
|
|
+ Text(row.subject)
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.78))
|
|
|
+ .lineLimit(2)
|
|
|
+ }
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ Text(row.time)
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
+ }
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ previewActionButton(title: "Reply", systemImage: "arrowshape.turn.up.left") {
|
|
|
+ openComposeReply(subject: row.subject)
|
|
|
+ }
|
|
|
+ previewActionButton(title: "Archive", systemImage: "archivebox") {
|
|
|
+ openGmailSearch(query: row.searchQuery)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(10)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.28))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.1), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func previewActionButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ action()
|
|
|
+ }) {
|
|
|
+ HStack(spacing: 5) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 10, 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.14)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .allowsHitTesting(!isPreview)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Large (interactive)
|
|
|
+
|
|
|
+ @State private var searchText = ""
|
|
|
+ @State private var webURL: URL
|
|
|
+
|
|
|
+ init(app: LauncherApp, mode: GmailWidgetMode, gmailAccountSlot: Int = 0, isPreview: Bool = false) {
|
|
|
+ self.app = app
|
|
|
+ self.mode = mode
|
|
|
+ self.gmailAccountSlot = gmailAccountSlot
|
|
|
+ self.isPreview = isPreview
|
|
|
+ let base = app.webURL ?? URL(string: "https://mail.google.com")!
|
|
|
+ _webURL = State(initialValue: Self.defaultInboxURL(base: base, slot: gmailAccountSlot))
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func defaultInboxURL(base: URL, slot: Int) -> URL {
|
|
|
+ WidgetDeepLinkURL.resolved(base: base, actionPath: "/mail/u/{{u}}/#inbox", accountSlot: slot)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactiveBody: some View {
|
|
|
+ Group {
|
|
|
+ if isPreview {
|
|
|
+ interactivePreviewChrome
|
|
|
+ } else {
|
|
|
+ interactiveLiveChrome
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactivePreviewChrome: some View {
|
|
|
+ ZStack {
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color(red: 0.32, green: 0.14, blue: 0.12), Color(red: 0.12, green: 0.14, blue: 0.2)],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ VStack(spacing: 12) {
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Gmail inbox")
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ Text("Full inbox, search, and folder shortcuts load on your desktop.")
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
+ .padding(.horizontal, 8)
|
|
|
+ }
|
|
|
+ .padding(16)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactiveLiveChrome: some View {
|
|
|
+ ZStack {
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.35))
|
|
|
+
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ VStack(spacing: 8) {
|
|
|
+ interactiveHeader
|
|
|
+ interactiveSearchBar
|
|
|
+ smartShortcutsRow
|
|
|
+ folderShortcutsRow
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 6)
|
|
|
+ .padding(.vertical, 8)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
+ .fill(Color(red: 0.07, green: 0.08, blue: 0.1))
|
|
|
+ )
|
|
|
+ .zIndex(20)
|
|
|
+ .allowsHitTesting(true)
|
|
|
+
|
|
|
+ GmailWebWidgetView(url: $webURL)
|
|
|
+ .frame(minHeight: 280)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
|
+ .zIndex(0)
|
|
|
+ }
|
|
|
+ .padding(10)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactiveHeader: some View {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Gmail")
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openMailInBrowser()
|
|
|
+ }) {
|
|
|
+ Image(systemName: "envelope.open")
|
|
|
+ .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 Mail in app browser")
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openCompose()
|
|
|
+ }) {
|
|
|
+ Image(systemName: "square.and.pencil")
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.9))
|
|
|
+ .padding(8)
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.12)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .help("Compose")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactiveSearchBar: some View {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: "magnifyingglass")
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ TextField("Search mail", text: $searchText)
|
|
|
+ .textFieldStyle(.plain)
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .onSubmit { submitSearch() }
|
|
|
+ Button("Go") {
|
|
|
+ submitSearch()
|
|
|
+ }
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
+ .foregroundStyle(Color(red: 0.95, green: 0.45, blue: 0.35))
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 10)
|
|
|
+ .padding(.vertical, 8)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
+ .fill(Color.white.opacity(0.08))
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private var smartShortcutsRow: some View {
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ shortcutChip("Primary", fragment: "#category/primary")
|
|
|
+ shortcutChip("Social", fragment: "#category/social")
|
|
|
+ shortcutChip("Promotions", fragment: "#category/promotions")
|
|
|
+ shortcutChip("Updates", fragment: "#category/updates")
|
|
|
+ shortcutChip("Forums", fragment: "#category/forums")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var folderShortcutsRow: some View {
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ shortcutChip("Inbox", fragment: "#inbox")
|
|
|
+ shortcutChip("Starred", fragment: "#starred")
|
|
|
+ shortcutChip("Snoozed", fragment: "#snoozed")
|
|
|
+ shortcutChip("Sent", fragment: "#sent")
|
|
|
+ shortcutChip("Drafts", fragment: "#drafts")
|
|
|
+ shortcutChip("All Mail", fragment: "#all")
|
|
|
+ shortcutChip("Spam", fragment: "#spam")
|
|
|
+ shortcutChip("Trash", fragment: "#trash")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func shortcutChip(_ title: String, fragment: String) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ navigateToMailPath("/mail/u/{{u}}/\(fragment)")
|
|
|
+ }) {
|
|
|
+ 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)))
|
|
|
+ .overlay(Capsule().stroke(Color.white.opacity(0.14), lineWidth: 1))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Shared chrome
|
|
|
+
|
|
|
+ private var gmailGradientBackground: some View {
|
|
|
+ LinearGradient(
|
|
|
+ colors: [
|
|
|
+ Color(red: 0.42, green: 0.2, blue: 0.18),
|
|
|
+ Color(red: 0.14, green: 0.18, blue: 0.26),
|
|
|
+ ],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func compactButton(title: String, systemImage: String, emphasized: Bool, action: @escaping () -> Void) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ action()
|
|
|
+ }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(emphasized ? Color(red: 0.12, green: 0.12, blue: 0.14) : .white.opacity(0.92))
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(emphasized ? Color.white.opacity(0.92) : Color.black.opacity(0.35))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(emphasized ? 0.35 : 0.14), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Navigation
|
|
|
+
|
|
|
+ private func openMailInBrowser() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let url = WidgetDeepLinkURL.resolved(
|
|
|
+ base: baseURL,
|
|
|
+ actionPath: "/mail/u/{{u}}/#inbox",
|
|
|
+ accountSlot: gmailAccountSlot
|
|
|
+ )
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openCompose() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let url = WidgetDeepLinkURL.resolved(
|
|
|
+ base: baseURL,
|
|
|
+ actionPath: "/mail/u/{{u}}/#inbox?compose=new",
|
|
|
+ accountSlot: gmailAccountSlot
|
|
|
+ )
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openUnreadSearch() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let url = WidgetDeepLinkURL.resolved(
|
|
|
+ base: baseURL,
|
|
|
+ actionPath: "/mail/u/{{u}}/#search/is:unread",
|
|
|
+ accountSlot: gmailAccountSlot
|
|
|
+ )
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openComposeReply(subject: String) {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ var c = URLComponents()
|
|
|
+ c.scheme = "https"
|
|
|
+ c.host = "mail.google.com"
|
|
|
+ c.path = "/mail/u/\(gmailAccountSlot)/"
|
|
|
+ c.queryItems = [
|
|
|
+ URLQueryItem(name: "view", value: "cm"),
|
|
|
+ URLQueryItem(name: "fs", value: "1"),
|
|
|
+ URLQueryItem(name: "su", value: "Re: \(subject)"),
|
|
|
+ ]
|
|
|
+ guard let url = c.url else { return }
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openGmailSearch(query: String) {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let encoded = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed) ?? query
|
|
|
+ let path = "/mail/u/{{u}}/#search/\(encoded)"
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: gmailAccountSlot)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func navigateToMailPath(_ path: String) {
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: gmailAccountSlot)
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ webURL = url
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func submitSearch() {
|
|
|
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ guard !q.isEmpty else { return }
|
|
|
+ let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed) ?? q
|
|
|
+ navigateToMailPath("/mail/u/{{u}}/#search/\(encoded)")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - WKWebView
|
|
|
+
|
|
|
+private final class GmailWebViewClippingContainer: NSView {
|
|
|
+ override var isFlipped: Bool { true }
|
|
|
+}
|
|
|
+
|
|
|
+private struct GmailWebWidgetView: NSViewRepresentable {
|
|
|
+ @Binding var url: URL
|
|
|
+
|
|
|
+ func makeCoordinator() -> Coordinator {
|
|
|
+ Coordinator()
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeNSView(context: Context) -> NSView {
|
|
|
+ let container = GmailWebViewClippingContainer()
|
|
|
+ container.wantsLayer = true
|
|
|
+ container.layer?.masksToBounds = true
|
|
|
+ container.layer?.cornerRadius = 12
|
|
|
+ container.layer?.borderWidth = 0
|
|
|
+
|
|
|
+ let config = WKWebViewConfiguration()
|
|
|
+ config.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
|
+ let wv = WKWebView(frame: .zero, configuration: config)
|
|
|
+ wv.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ wv.focusRingType = .none
|
|
|
+ wv.customUserAgent =
|
|
|
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
|
|
|
+ wv.navigationDelegate = context.coordinator
|
|
|
+
|
|
|
+ container.addSubview(wv)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ wv.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
+ wv.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
+ wv.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
+ wv.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
+ ])
|
|
|
+
|
|
|
+ context.coordinator.webView = wv
|
|
|
+ context.coordinator.lastLoadedURLString = url.absoluteString
|
|
|
+ wv.load(URLRequest(url: url))
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ Self.stripWebKitOuterChrome(wv)
|
|
|
+ }
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
|
|
+ Self.stripWebKitOuterChrome(wv)
|
|
|
+ }
|
|
|
+ return container
|
|
|
+ }
|
|
|
+
|
|
|
+ func updateNSView(_ container: NSView, context: Context) {
|
|
|
+ guard let wv = context.coordinator.webView else { return }
|
|
|
+ let next = url.absoluteString
|
|
|
+ guard context.coordinator.lastLoadedURLString != next else { return }
|
|
|
+ context.coordinator.lastLoadedURLString = next
|
|
|
+ wv.load(URLRequest(url: url))
|
|
|
+ }
|
|
|
+
|
|
|
+ /// WebKit embeds an `NSScrollView` that often draws a 1pt dark border around the web area (visible on large widgets).
|
|
|
+ private static func stripWebKitOuterChrome(_ webView: WKWebView) {
|
|
|
+ webView.focusRingType = .none
|
|
|
+ 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(webView)
|
|
|
+ }
|
|
|
+
|
|
|
+ final class Coordinator: NSObject, WKNavigationDelegate {
|
|
|
+ weak var webView: WKWebView?
|
|
|
+ var lastLoadedURLString: String?
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
+ GmailWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
+ GmailWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|