|
|
@@ -1,6 +1,5 @@
|
|
|
import AppKit
|
|
|
import SwiftUI
|
|
|
-import WebKit
|
|
|
|
|
|
// MARK: - Public SwiftUI view
|
|
|
|
|
|
@@ -85,20 +84,6 @@ struct GmailDesktopWidgetView: View {
|
|
|
// 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 {
|
|
|
@@ -120,12 +105,12 @@ struct GmailDesktopWidgetView: View {
|
|
|
endPoint: .bottomTrailing
|
|
|
)
|
|
|
)
|
|
|
- VStack(spacing: 12) {
|
|
|
+ VStack(spacing: 10) {
|
|
|
AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
- Text("Gmail inbox")
|
|
|
+ Text("Gmail command center")
|
|
|
.font(.system(size: 15, weight: .bold))
|
|
|
.foregroundStyle(.white.opacity(0.92))
|
|
|
- Text("Full inbox, search, and folder shortcuts load on your desktop.")
|
|
|
+ Text("Fast actions, inbox shortcuts, and clean navigation built for desktop.")
|
|
|
.font(.system(size: 11, weight: .medium))
|
|
|
.foregroundStyle(.white.opacity(0.65))
|
|
|
.multilineTextAlignment(.center)
|
|
|
@@ -137,32 +122,22 @@ struct GmailDesktopWidgetView: View {
|
|
|
|
|
|
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))
|
|
|
+ gmailGradientBackground
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.38))
|
|
|
)
|
|
|
- .zIndex(20)
|
|
|
- .allowsHitTesting(true)
|
|
|
-
|
|
|
- GmailWebWidgetView(url: $webURL)
|
|
|
- .frame(minHeight: 280)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
- .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
|
- .zIndex(0)
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 12) {
|
|
|
+ interactiveHeader
|
|
|
+ interactiveSearchBar
|
|
|
+ quickActionGrid
|
|
|
+ mailboxRow
|
|
|
+ categoryRow
|
|
|
+ footerRow
|
|
|
+ Spacer(minLength: 0)
|
|
|
}
|
|
|
- .padding(10)
|
|
|
+ .padding(14)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -178,10 +153,10 @@ struct GmailDesktopWidgetView: View {
|
|
|
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)))
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ .frame(width: 34, height: 34)
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.14)))
|
|
|
}
|
|
|
.buttonStyle(.plain)
|
|
|
.help("Open Mail in app browser")
|
|
|
@@ -190,10 +165,10 @@ struct GmailDesktopWidgetView: View {
|
|
|
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)))
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ .frame(width: 34, height: 34)
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.14)))
|
|
|
}
|
|
|
.buttonStyle(.plain)
|
|
|
.help("Compose")
|
|
|
@@ -210,59 +185,160 @@ struct GmailDesktopWidgetView: View {
|
|
|
.font(.system(size: 12, weight: .medium))
|
|
|
.foregroundStyle(.white.opacity(0.95))
|
|
|
.onSubmit { submitSearch() }
|
|
|
- Button("Go") {
|
|
|
+ Button("Search") {
|
|
|
submitSearch()
|
|
|
}
|
|
|
- .font(.system(size: 11, weight: .bold))
|
|
|
- .foregroundStyle(Color(red: 0.95, green: 0.45, blue: 0.35))
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
+ .foregroundStyle(Color(red: 0.97, green: 0.62, blue: 0.53))
|
|
|
.buttonStyle(.plain)
|
|
|
}
|
|
|
.padding(.horizontal, 10)
|
|
|
.padding(.vertical, 8)
|
|
|
.background(
|
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
- .fill(Color.white.opacity(0.08))
|
|
|
+ .fill(Color.black.opacity(0.32))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- 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 quickActionGrid: some View {
|
|
|
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 0), spacing: 8), count: 2), spacing: 8) {
|
|
|
+ quickActionCard(
|
|
|
+ title: "Unread",
|
|
|
+ subtitle: unreadCountStored < 0 ? "Syncing" : "\(unreadCountStored) messages",
|
|
|
+ systemImage: "envelope.badge.fill"
|
|
|
+ ) {
|
|
|
+ openUnreadSearch()
|
|
|
+ }
|
|
|
+ quickActionCard(
|
|
|
+ title: "Compose",
|
|
|
+ subtitle: "Start a new message",
|
|
|
+ systemImage: "square.and.pencil"
|
|
|
+ ) {
|
|
|
+ openCompose()
|
|
|
+ }
|
|
|
+ quickActionCard(
|
|
|
+ title: "Starred",
|
|
|
+ subtitle: "Important mail",
|
|
|
+ systemImage: "star.fill"
|
|
|
+ ) {
|
|
|
+ openMailbox("#starred")
|
|
|
+ }
|
|
|
+ quickActionCard(
|
|
|
+ title: "Drafts",
|
|
|
+ subtitle: "Continue writing",
|
|
|
+ systemImage: "doc.text.fill"
|
|
|
+ ) {
|
|
|
+ openMailbox("#drafts")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var mailboxRow: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
+ Text("Mailboxes")
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ shortcutChip("Inbox", fragment: "#inbox")
|
|
|
+ shortcutChip("Sent", fragment: "#sent")
|
|
|
+ shortcutChip("Snoozed", fragment: "#snoozed")
|
|
|
+ shortcutChip("All Mail", fragment: "#all")
|
|
|
+ shortcutChip("Spam", fragment: "#spam")
|
|
|
+ shortcutChip("Trash", fragment: "#trash")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var categoryRow: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
+ Text("Categories")
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
+ 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 var footerRow: some View {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openMailInBrowser()
|
|
|
+ }) {
|
|
|
+ Label("Open Full Gmail", systemImage: "arrow.up.right.square")
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.94))
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.35))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.14), lineWidth: 1)
|
|
|
+ )
|
|
|
}
|
|
|
+ .buttonStyle(.plain)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func quickActionCard(title: String, subtitle: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ action()
|
|
|
+ }) {
|
|
|
+ VStack(alignment: .leading, spacing: 5) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.9))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .lineLimit(1)
|
|
|
+ Text(subtitle)
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.7))
|
|
|
+ .lineLimit(1)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .padding(10)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.3))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.13), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
private func shortcutChip(_ title: String, fragment: String) -> some View {
|
|
|
Button(action: {
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
- navigateToMailPath("/mail/u/{{u}}/\(fragment)")
|
|
|
+ openMailbox(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)))
|
|
|
+ .background(Capsule().fill(Color.black.opacity(0.3)))
|
|
|
.overlay(Capsule().stroke(Color.white.opacity(0.14), lineWidth: 1))
|
|
|
}
|
|
|
.buttonStyle(.plain)
|
|
|
@@ -331,117 +407,23 @@ struct GmailDesktopWidgetView: View {
|
|
|
|
|
|
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)
|
|
|
+ openPath("/mail/u/{{u}}/#search/is:unread")
|
|
|
}
|
|
|
|
|
|
- private func navigateToMailPath(_ path: String) {
|
|
|
+ private func openMailbox(_ fragment: String) {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ openPath("/mail/u/{{u}}/\(fragment)")
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openPath(_ path: String) {
|
|
|
let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: gmailAccountSlot)
|
|
|
- DispatchQueue.main.async {
|
|
|
- webURL = url
|
|
|
- }
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
- }
|
|
|
- }
|
|
|
+ openPath("/mail/u/{{u}}/#search/\(encoded)")
|
|
|
}
|
|
|
}
|