|
|
@@ -0,0 +1,518 @@
|
|
|
+import AppKit
|
|
|
+import SwiftUI
|
|
|
+import WebKit
|
|
|
+
|
|
|
+/// Google Keep desktop widget: recent notes (small) or embedded Keep (large).
|
|
|
+struct KeepDesktopWidgetView: View {
|
|
|
+ let app: LauncherApp
|
|
|
+ let mode: KeepWidgetMode
|
|
|
+ var isPreview: Bool = false
|
|
|
+
|
|
|
+ @ObservedObject private var liveNotes = KeepLiveNotesStore.shared
|
|
|
+
|
|
|
+ private var baseURL: URL { app.webURL ?? URL(string: "https://keep.google.com")! }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Group {
|
|
|
+ switch mode {
|
|
|
+ case .compact:
|
|
|
+ compactBody
|
|
|
+ case .interactive:
|
|
|
+ interactiveBody
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Small
|
|
|
+
|
|
|
+ /// Static rows for widget gallery / preview only.
|
|
|
+ private static let previewCatalogNotes: [KeepLiveNotesStore.Note] = [
|
|
|
+ KeepLiveNotesStore.Note(id: "p1", title: "Groceries", snippet: "Milk, eggs, coffee…"),
|
|
|
+ KeepLiveNotesStore.Note(id: "p2", title: "Meeting notes", snippet: "Follow up with design…"),
|
|
|
+ KeepLiveNotesStore.Note(id: "p3", title: "Ideas", snippet: "Widget themes for spring…"),
|
|
|
+ ]
|
|
|
+
|
|
|
+ private var compactDisplayNotes: [KeepLiveNotesStore.Note] {
|
|
|
+ if isPreview { return Array(Self.previewCatalogNotes.prefix(3)) }
|
|
|
+ return Array(liveNotes.notes.prefix(3))
|
|
|
+ }
|
|
|
+
|
|
|
+ private var compactBody: some View {
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
+ keepGradientBackground
|
|
|
+ .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("Recent")
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ }
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ if !isPreview {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ liveNotes.forceRefresh()
|
|
|
+ }) {
|
|
|
+ Image(systemName: "arrow.clockwise")
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.75))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .help("Refresh from Keep")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ keepStatusCaption
|
|
|
+
|
|
|
+ compactNoteList
|
|
|
+
|
|
|
+ keepPrimaryButton(title: "New note", systemImage: "square.and.pencil") {
|
|
|
+ openKeepFragment("NOTE")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(14)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .opacity(isPreview ? 0.85 : 1)
|
|
|
+ .onAppear {
|
|
|
+ if !isPreview {
|
|
|
+ liveNotes.requestRefreshIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder
|
|
|
+ private var keepStatusCaption: some View {
|
|
|
+ if isPreview {
|
|
|
+ Text("Preview")
|
|
|
+ .font(.system(size: 9, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.45))
|
|
|
+ } else {
|
|
|
+ switch liveNotes.phase {
|
|
|
+ case .idle, .loading:
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ ProgressView()
|
|
|
+ .controlSize(.small)
|
|
|
+ .scaleEffect(0.75)
|
|
|
+ Text("Loading from Keep…")
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ }
|
|
|
+ case .ready:
|
|
|
+ Text("From your Keep")
|
|
|
+ .font(.system(size: 9, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
+ case .empty:
|
|
|
+ Text("No notes found")
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ case .needsSignIn:
|
|
|
+ Text("Sign in via large Keep widget")
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(Color(red: 1, green: 0.85, blue: 0.4).opacity(0.95))
|
|
|
+ .lineLimit(2)
|
|
|
+ case .failed:
|
|
|
+ Text("Could not load — tap refresh")
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder
|
|
|
+ private var compactNoteList: some View {
|
|
|
+ if !isPreview, case .needsSignIn = liveNotes.phase {
|
|
|
+ Text("Open the large Keep widget and sign in to Google. This widget reads notes from the same session as that web view.")
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.72))
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+ } else if compactDisplayNotes.isEmpty, !isPreview, liveNotes.phase == .ready || liveNotes.phase == .empty {
|
|
|
+ Text("No notes yet, or Keep changed its layout. Try refresh or open Keep in the large widget.")
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+ } else {
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
+ ForEach(compactDisplayNotes) { note in
|
|
|
+ VStack(alignment: .leading, spacing: 4) {
|
|
|
+ Text(note.title.isEmpty ? "Note" : note.title)
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .lineLimit(1)
|
|
|
+ Text(note.snippet.isEmpty ? " " : note.snippet)
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .lineLimit(2)
|
|
|
+ }
|
|
|
+ .padding(10)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.2)))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Large (WKWebView)
|
|
|
+
|
|
|
+ @State private var searchText = ""
|
|
|
+ @State private var webURL: URL
|
|
|
+
|
|
|
+ init(app: LauncherApp, mode: KeepWidgetMode, isPreview: Bool = false) {
|
|
|
+ self.app = app
|
|
|
+ self.mode = mode
|
|
|
+ self.isPreview = isPreview
|
|
|
+ let base = app.webURL ?? URL(string: "https://keep.google.com")!
|
|
|
+ _webURL = State(initialValue: WidgetDeepLinkURL.resolved(base: base, actionPath: "/", accountSlot: 0))
|
|
|
+ }
|
|
|
+
|
|
|
+ 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.42, green: 0.35, blue: 0.12), Color(red: 0.2, green: 0.18, blue: 0.1)],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ VStack(spacing: 12) {
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ Text("Google Keep")
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
+ Text("Full Keep with notes, labels, and lists loads 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.28))
|
|
|
+
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ VStack(spacing: 8) {
|
|
|
+ interactiveHeader
|
|
|
+ interactiveSearchBar
|
|
|
+ filterChipsRow
|
|
|
+ createRow
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 6)
|
|
|
+ .padding(.vertical, 8)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
+ .fill(Color(red: 0.12, green: 0.1, blue: 0.06))
|
|
|
+ )
|
|
|
+ .zIndex(20)
|
|
|
+
|
|
|
+ KeepWebWidgetView(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("Keep")
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ Spacer(minLength: 0)
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openKeepInBrowser()
|
|
|
+ }) {
|
|
|
+ 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 Keep in browser")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var interactiveSearchBar: some View {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: "magnifyingglass")
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ TextField("Search notes", 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: 1, green: 0.85, 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 filterChipsRow: some View {
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ keepChip("Notes", fragment: "")
|
|
|
+ keepChip("Reminders", fragment: "reminders")
|
|
|
+ keepChip("Archive", fragment: "archive")
|
|
|
+ keepChip("Trash", fragment: "trash")
|
|
|
+ keepChip("Labels", fragment: "label")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var createRow: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
+ Text("Create")
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ createChip("Text", fragment: "NOTE")
|
|
|
+ createChip("List", fragment: "LIST")
|
|
|
+ createChip("Drawing", fragment: "DRAWING")
|
|
|
+ createChip("Image", fragment: "NOTE")
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openKeepFragment("NOTE")
|
|
|
+ }) {
|
|
|
+ HStack(spacing: 4) {
|
|
|
+ Image(systemName: "photo")
|
|
|
+ Text("Photo")
|
|
|
+ }
|
|
|
+ .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)
|
|
|
+ .help("New note — add an image in Keep")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func keepChip(_ title: String, fragment: String) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ if fragment.isEmpty {
|
|
|
+ navigateWeb(to: "/")
|
|
|
+ } else {
|
|
|
+ navigateWeb(to: "/#\(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)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func createChip(_ title: String, fragment: String) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ openKeepFragment(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)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Shared styling
|
|
|
+
|
|
|
+ private var keepGradientBackground: some View {
|
|
|
+ LinearGradient(
|
|
|
+ colors: [
|
|
|
+ Color(red: 0.52, green: 0.42, blue: 0.14),
|
|
|
+ Color(red: 0.22, green: 0.2, blue: 0.12),
|
|
|
+ ],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func keepPrimaryButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
+ Button(action: {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ action()
|
|
|
+ }) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 13, weight: .bold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(Color(red: 0.12, green: 0.1, blue: 0.04))
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 11)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(Color.white.opacity(0.92))
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .disabled(isPreview)
|
|
|
+ .allowsHitTesting(!isPreview)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - URLs
|
|
|
+
|
|
|
+ private func openKeepFragment(_ fragment: String) {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: "/#\(fragment)", accountSlot: 0)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func openKeepInBrowser() {
|
|
|
+ guard !isPreview else { return }
|
|
|
+ InAppBrowserWindowManager.shared.open(url: baseURL, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func navigateWeb(to path: String) {
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ webURL = url
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func submitSearch() {
|
|
|
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ guard !q.isEmpty else { return }
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed) ?? q
|
|
|
+ navigateWeb(to: "/#search/\(encoded)")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - WKWebView
|
|
|
+
|
|
|
+private final class KeepWebViewClippingContainer: NSView {
|
|
|
+ override var isFlipped: Bool { true }
|
|
|
+}
|
|
|
+
|
|
|
+private struct KeepWebWidgetView: NSViewRepresentable {
|
|
|
+ @Binding var url: URL
|
|
|
+
|
|
|
+ func makeCoordinator() -> Coordinator {
|
|
|
+ Coordinator()
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeNSView(context: Context) -> NSView {
|
|
|
+ let container = KeepWebViewClippingContainer()
|
|
|
+ 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))
|
|
|
+ }
|
|
|
+
|
|
|
+ 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!) {
|
|
|
+ KeepWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
+ KeepWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|