|
@@ -0,0 +1,469 @@
|
|
|
|
|
+import AppKit
|
|
|
|
|
+import Combine
|
|
|
|
|
+import SwiftUI
|
|
|
|
|
+import WebKit
|
|
|
|
|
+
|
|
|
|
|
+private extension Notification.Name {
|
|
|
|
|
+ /// Posted when Translate’s web view finishes loading (object: `WKWebView`).
|
|
|
|
|
+ static let translateWidgetPageLoaded = Notification.Name("google_apps.translateWidgetPageLoaded")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Holds the embedded translator `WKWebView` so toolbar actions (voice, conversation JS) can target it.
|
|
|
|
|
+private final class TranslateWebViewRef: ObservableObject {
|
|
|
|
|
+ weak var webView: WKWebView?
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Large interactive Google Translate: full web UI in-panel (text, mic, conversation, history) plus local recent chips.
|
|
|
|
|
+struct TranslateDesktopWidgetView: View {
|
|
|
|
|
+ let app: LauncherApp
|
|
|
|
|
+ let mode: TranslateWidgetMode
|
|
|
|
|
+ var isPreview: Bool = false
|
|
|
|
|
+
|
|
|
|
|
+ @StateObject private var webRef = TranslateWebViewRef()
|
|
|
|
|
+ @State private var webURL: URL
|
|
|
|
|
+ @State private var recentEntries: [TranslateRecentEntry] = []
|
|
|
|
|
+
|
|
|
|
|
+ private var baseURL: URL { app.webURL ?? URL(string: "https://translate.google.com")! }
|
|
|
|
|
+
|
|
|
|
|
+ private static let recentDefaultsKey = "translateWidgetRecentJSON"
|
|
|
|
|
+
|
|
|
|
|
+ init(app: LauncherApp, mode: TranslateWidgetMode, isPreview: Bool = false) {
|
|
|
|
|
+ self.app = app
|
|
|
|
|
+ self.mode = mode
|
|
|
|
|
+ self.isPreview = isPreview
|
|
|
|
|
+ let base = app.webURL ?? URL(string: "https://translate.google.com")!
|
|
|
|
|
+ _webURL = State(
|
|
|
|
|
+ initialValue: WidgetDeepLinkURL.resolved(base: base, actionPath: "/?sl=auto&tl=en", accountSlot: 0)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ switch mode {
|
|
|
|
|
+ case .interactive:
|
|
|
|
|
+ interactiveBody
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
|
|
+ .onAppear {
|
|
|
|
|
+ loadRecents()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveBody: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ if isPreview {
|
|
|
|
|
+ translatePreviewChrome
|
|
|
|
|
+ } else {
|
|
|
|
|
+ translateLiveChrome
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var translatePreviewChrome: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(
|
|
|
|
|
+ LinearGradient(
|
|
|
|
|
+ colors: [Color(red: 0.2, green: 0.45, blue: 0.95), Color(red: 0.08, green: 0.22, blue: 0.55)],
|
|
|
|
|
+ startPoint: .topLeading,
|
|
|
|
|
+ endPoint: .bottomTrailing
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ VStack(spacing: 12) {
|
|
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Google Translate")
|
|
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ Text("Type, translate documents, and browse history — all inside this widget.")
|
|
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.7))
|
|
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
|
|
+ .padding(.horizontal, 8)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var translateLiveChrome: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(Color.black.opacity(0.22))
|
|
|
|
|
+
|
|
|
|
|
+ VStack(spacing: 0) {
|
|
|
|
|
+ translateToolbar
|
|
|
|
|
+ recentStrip
|
|
|
|
|
+ TranslateWebWidgetView(url: $webURL, webRef: webRef)
|
|
|
|
|
+ .frame(minHeight: 320)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(10)
|
|
|
|
|
+ }
|
|
|
|
|
+ .onReceive(NotificationCenter.default.publisher(for: .translateWidgetPageLoaded)) { note in
|
|
|
|
|
+ guard let wv = note.object as? WKWebView, wv === webRef.webView else { return }
|
|
|
|
|
+ extractAndMergeRecents(webView: wv)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var translateToolbar: some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Translate")
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ 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 Translate in browser")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ toolbarChip("Text", systemImage: "textformat", path: "/?sl=auto&tl=en", action: nil)
|
|
|
|
|
+ toolbarChip("Documents", systemImage: "doc.text", path: "/?tr=f&hl=en&tab=wT", action: nil)
|
|
|
|
|
+ toolbarChip("History", systemImage: "clock.arrow.circlepath", path: nil, action: { runOpenHistory() })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 6)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
|
|
+ .fill(Color(red: 0.08, green: 0.14, blue: 0.28))
|
|
|
|
|
+ )
|
|
|
|
|
+ .zIndex(20)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @ViewBuilder
|
|
|
|
|
+ private var recentStrip: some View {
|
|
|
|
|
+ if recentEntries.isEmpty {
|
|
|
|
|
+ EmptyView()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 4) {
|
|
|
|
|
+ Text("Recent")
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ ForEach(recentEntries) { entry in
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ applyRecentEntry(entry)
|
|
|
|
|
+ }) {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
+ Text(entry.sourcePreview)
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+ Text(entry.targetPreview)
|
|
|
|
|
+ .font(.system(size: 9, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.6))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.1)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 6)
|
|
|
|
|
+ .padding(.bottom, 6)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func toolbarChip(
|
|
|
|
|
+ _ title: String,
|
|
|
|
|
+ systemImage: String,
|
|
|
|
|
+ path: String?,
|
|
|
|
|
+ action: (() -> Void)?
|
|
|
|
|
+ ) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ if let action {
|
|
|
|
|
+ action()
|
|
|
|
|
+ } else if let path {
|
|
|
|
|
+ navigateWeb(to: path)
|
|
|
|
|
+ }
|
|
|
|
|
+ }) {
|
|
|
|
|
+ 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)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - URLs & actions
|
|
|
|
|
+
|
|
|
|
|
+ private func navigateWeb(to path: String) {
|
|
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
|
|
|
|
|
+ DispatchQueue.main.async {
|
|
|
|
|
+ webURL = url
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openInAppBrowser(_ url: URL) {
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func runOpenHistory() {
|
|
|
|
|
+ guard let wv = webRef.webView else { return }
|
|
|
|
|
+ wv.evaluateJavaScript(Self.clickHistoryJavaScript, completionHandler: nil)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func applyRecentEntry(_ entry: TranslateRecentEntry) {
|
|
|
|
|
+ navigateWeb(to: "/?sl=auto&tl=en")
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
|
|
|
|
+ guard let wv = webRef.webView else { return }
|
|
|
|
|
+ guard let jsonData = try? JSONEncoder().encode([entry.source]),
|
|
|
|
|
+ let jsonLiteral = String(data: jsonData, encoding: .utf8)
|
|
|
|
|
+ else { return }
|
|
|
|
|
+ let script = """
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ var arr = \(jsonLiteral);
|
|
|
|
|
+ var text = arr[0] || '';
|
|
|
|
|
+ var ta = document.querySelector('textarea');
|
|
|
|
|
+ if (!ta) return;
|
|
|
|
|
+ ta.focus();
|
|
|
|
|
+ ta.value = text;
|
|
|
|
|
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
|
+ })();
|
|
|
|
|
+ """
|
|
|
|
|
+ wv.evaluateJavaScript(script, completionHandler: nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Recent translations (local)
|
|
|
|
|
+
|
|
|
|
|
+ private func loadRecents() {
|
|
|
|
|
+ guard let data = UserDefaults.standard.string(forKey: Self.recentDefaultsKey)?.data(using: .utf8),
|
|
|
|
|
+ let decoded = try? JSONDecoder().decode([TranslateRecentEntry].self, from: data)
|
|
|
|
|
+ else {
|
|
|
|
|
+ recentEntries = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ recentEntries = decoded
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func saveRecents() {
|
|
|
|
|
+ guard let data = try? JSONEncoder().encode(recentEntries),
|
|
|
|
|
+ let str = String(data: data, encoding: .utf8)
|
|
|
|
|
+ else { return }
|
|
|
|
|
+ UserDefaults.standard.set(str, forKey: Self.recentDefaultsKey)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func extractAndMergeRecents(webView: WKWebView) {
|
|
|
|
|
+ webView.evaluateJavaScript(Self.extractPairJavaScript) { result, _ in
|
|
|
|
|
+ Task { @MainActor in
|
|
|
|
|
+ guard let json = result as? String,
|
|
|
|
|
+ let data = json.data(using: .utf8),
|
|
|
|
|
+ let pair = try? JSONDecoder().decode(ExtractedPair.self, from: data),
|
|
|
|
|
+ pair.source.count > 0,
|
|
|
|
|
+ pair.target.count > 1
|
|
|
|
|
+ else { return }
|
|
|
|
|
+
|
|
|
|
|
+ var next = recentEntries
|
|
|
|
|
+ if let idx = next.firstIndex(where: { $0.source == pair.source }) {
|
|
|
|
|
+ next[idx] = TranslateRecentEntry(
|
|
|
|
|
+ id: next[idx].id,
|
|
|
|
|
+ source: pair.source,
|
|
|
|
|
+ target: pair.target,
|
|
|
|
|
+ savedAt: Date()
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let entry = TranslateRecentEntry(id: UUID(), source: pair.source, target: pair.target, savedAt: Date())
|
|
|
|
|
+ next.insert(entry, at: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+ if next.count > 8 {
|
|
|
|
|
+ next = Array(next.prefix(8))
|
|
|
|
|
+ }
|
|
|
|
|
+ recentEntries = next
|
|
|
|
|
+ saveRecents()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private struct ExtractedPair: Decodable {
|
|
|
|
|
+ var source: String
|
|
|
|
|
+ var target: String
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static let extractPairJavaScript = #"""
|
|
|
|
|
+(function(){
|
|
|
|
|
+ try {
|
|
|
|
|
+ var tas = document.querySelectorAll('textarea');
|
|
|
|
|
+ var src = '';
|
|
|
|
|
+ var dst = '';
|
|
|
|
|
+ if (tas.length >= 1) src = (tas[0].value || tas[0].innerText || '').trim();
|
|
|
|
|
+ if (tas.length >= 2) dst = (tas[1].value || tas[1].innerText || '').trim();
|
|
|
|
|
+ if (!dst) {
|
|
|
|
|
+ var main = document.querySelector('[role="main"]');
|
|
|
|
|
+ if (main) {
|
|
|
|
|
+ var spans = main.querySelectorAll('span[lang], span[jsname]');
|
|
|
|
|
+ if (spans.length) {
|
|
|
|
|
+ var parts = [];
|
|
|
|
|
+ for (var i = 0; i < Math.min(spans.length, 6); i++) {
|
|
|
|
|
+ var t = (spans[i].innerText || '').trim();
|
|
|
|
|
+ if (t) parts.push(t);
|
|
|
|
|
+ }
|
|
|
|
|
+ dst = parts.join(' ').trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return JSON.stringify({ source: src, target: dst });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return JSON.stringify({ source: '', target: '' });
|
|
|
|
|
+ }
|
|
|
|
|
+})();
|
|
|
|
|
+"""#
|
|
|
|
|
+
|
|
|
|
|
+ private static let clickHistoryJavaScript = #"""
|
|
|
|
|
+(function(){
|
|
|
|
|
+ var nodes = document.querySelectorAll('button, [role="tab"], a, div[role="button"]');
|
|
|
|
|
+ for (var i = 0; i < nodes.length; i++) {
|
|
|
|
|
+ var b = nodes[i];
|
|
|
|
|
+ var t = (b.innerText || b.getAttribute('aria-label') || '') + '';
|
|
|
|
|
+ if (/^history$/i.test(t.trim()) || /translation\s+history/i.test(t)) { b.click(); return 'ok'; }
|
|
|
|
|
+ }
|
|
|
|
|
+ return 'no';
|
|
|
|
|
+})();
|
|
|
|
|
+"""#
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private struct TranslateRecentEntry: Codable, Identifiable, Equatable {
|
|
|
|
|
+ var id: UUID
|
|
|
|
|
+ var source: String
|
|
|
|
|
+ var target: String
|
|
|
|
|
+ var savedAt: Date
|
|
|
|
|
+
|
|
|
|
|
+ var sourcePreview: String {
|
|
|
|
|
+ String(source.prefix(42))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var targetPreview: String {
|
|
|
|
|
+ String(target.prefix(42))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// MARK: - WKWebView
|
|
|
|
|
+
|
|
|
|
|
+private final class TranslateWebViewClippingContainer: NSView {
|
|
|
|
|
+ override var isFlipped: Bool { true }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private struct TranslateWebWidgetView: NSViewRepresentable {
|
|
|
|
|
+ @Binding var url: URL
|
|
|
|
|
+ var webRef: TranslateWebViewRef
|
|
|
|
|
+
|
|
|
|
|
+ func makeCoordinator() -> Coordinator {
|
|
|
|
|
+ Coordinator(webRef: webRef)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func makeNSView(context: Context) -> NSView {
|
|
|
|
|
+ let container = TranslateWebViewClippingContainer()
|
|
|
|
|
+ container.wantsLayer = true
|
|
|
|
|
+ container.layer?.masksToBounds = true
|
|
|
|
|
+ container.layer?.cornerRadius = 12
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ webRef.webView = wv
|
|
|
|
|
+ context.coordinator.lastLoadedURLString = url.absoluteString
|
|
|
|
|
+ wv.load(URLRequest(url: url))
|
|
|
|
|
+ DispatchQueue.main.async {
|
|
|
|
|
+ Self.stripWebKitOuterChrome(wv)
|
|
|
|
|
+ }
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
|
|
|
|
+ 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?
|
|
|
|
|
+ let webRef: TranslateWebViewRef
|
|
|
|
|
+
|
|
|
|
|
+ init(webRef: TranslateWebViewRef) {
|
|
|
|
|
+ self.webRef = webRef
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
|
|
+ self.webView = webView
|
|
|
|
|
+ webRef.webView = webView
|
|
|
|
|
+ TranslateWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) {
|
|
|
|
|
+ TranslateWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
|
|
+ }
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.4) {
|
|
|
|
|
+ NotificationCenter.default.post(name: .translateWidgetPageLoaded, object: webView)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|