|
@@ -14,6 +14,7 @@ private enum SidebarPage: Int {
|
|
|
case video = 2
|
|
case video = 2
|
|
|
case tutorials = 3
|
|
case tutorials = 3
|
|
|
case settings = 4
|
|
case settings = 4
|
|
|
|
|
+ case browse = 5
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private enum ZoomJoinMode: Int {
|
|
private enum ZoomJoinMode: Int {
|
|
@@ -59,8 +60,12 @@ final class ViewController: NSViewController {
|
|
|
private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
|
|
private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
|
|
|
private weak var paywallOfferLabel: NSTextField?
|
|
private weak var paywallOfferLabel: NSTextField?
|
|
|
private weak var meetLinkField: NSTextField?
|
|
private weak var meetLinkField: NSTextField?
|
|
|
|
|
+ private weak var browseAddressField: NSTextField?
|
|
|
private var inAppBrowserWindowController: InAppBrowserWindowController?
|
|
private var inAppBrowserWindowController: InAppBrowserWindowController?
|
|
|
|
|
|
|
|
|
|
+ /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
|
|
|
|
|
+ private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
|
|
+
|
|
|
private let darkModeDefaultsKey = "settings.darkModeEnabled"
|
|
private let darkModeDefaultsKey = "settings.darkModeEnabled"
|
|
|
private var darkModeEnabled: Bool {
|
|
private var darkModeEnabled: Bool {
|
|
|
get {
|
|
get {
|
|
@@ -184,8 +189,10 @@ private extension ViewController {
|
|
|
|
|
|
|
|
@objc private func joinMeetClicked(_ sender: Any?) {
|
|
@objc private func joinMeetClicked(_ sender: Any?) {
|
|
|
let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
- guard rawInput.isEmpty == false else {
|
|
|
|
|
- showSimpleAlert(title: "Join Meeting", message: "Please enter a Google Meet link.")
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if rawInput.isEmpty {
|
|
|
|
|
+ guard let url = URL(string: "https://meet.google.com/") else { return }
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -197,13 +204,42 @@ private extension ViewController {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- openInSafari(url: url)
|
|
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func cancelMeetJoinClicked(_ sender: Any?) {
|
|
@objc private func cancelMeetJoinClicked(_ sender: Any?) {
|
|
|
meetLinkField?.stringValue = ""
|
|
meetLinkField?.stringValue = ""
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @objc private func browseOpenAddressClicked(_ sender: Any?) {
|
|
|
|
|
+ let raw = browseAddressField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
+ guard raw.isEmpty == false else {
|
|
|
|
|
+ showSimpleAlert(title: "Browse", message: "Enter a web address (for example meet.google.com).")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ let normalized = normalizedURLString(from: raw)
|
|
|
|
|
+ guard let url = URL(string: normalized), url.scheme == "http" || url.scheme == "https" else {
|
|
|
|
|
+ showSimpleAlert(title: "Invalid address", message: "Enter a valid http or https URL.")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func browseQuickLinkMeetClicked(_ sender: Any?) {
|
|
|
|
|
+ guard let url = URL(string: "https://meet.google.com/") else { return }
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func browseQuickLinkMeetHelpClicked(_ sender: Any?) {
|
|
|
|
|
+ guard let url = URL(string: "https://support.google.com/meet") else { return }
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func browseQuickLinkZoomHelpClicked(_ sender: Any?) {
|
|
|
|
|
+ guard let url = URL(string: "https://support.zoom.us") else { return }
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func normalizedURLString(from value: String) -> String {
|
|
private func normalizedURLString(from value: String) -> String {
|
|
|
if value.lowercased().hasPrefix("http://") || value.lowercased().hasPrefix("https://") {
|
|
if value.lowercased().hasPrefix("http://") || value.lowercased().hasPrefix("https://") {
|
|
|
return value
|
|
return value
|
|
@@ -211,7 +247,7 @@ private extension ViewController {
|
|
|
return "https://\(value)"
|
|
return "https://\(value)"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func openInAppBrowser(with url: URL) {
|
|
|
|
|
|
|
+ private func openInAppBrowser(with url: URL, policy: InAppBrowserURLPolicy = .allowAll) {
|
|
|
let browserController: InAppBrowserWindowController
|
|
let browserController: InAppBrowserWindowController
|
|
|
if let existing = inAppBrowserWindowController {
|
|
if let existing = inAppBrowserWindowController {
|
|
|
browserController = existing
|
|
browserController = existing
|
|
@@ -220,7 +256,7 @@ private extension ViewController {
|
|
|
inAppBrowserWindowController = browserController
|
|
inAppBrowserWindowController = browserController
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- browserController.load(url: url)
|
|
|
|
|
|
|
+ browserController.load(url: url, policy: policy)
|
|
|
browserController.showWindow(nil)
|
|
browserController.showWindow(nil)
|
|
|
browserController.window?.makeKeyAndOrderFront(nil)
|
|
browserController.window?.makeKeyAndOrderFront(nil)
|
|
|
browserController.window?.orderFrontRegardless()
|
|
browserController.window?.orderFrontRegardless()
|
|
@@ -306,12 +342,25 @@ private extension ViewController {
|
|
|
case .restore:
|
|
case .restore:
|
|
|
showSimpleAlert(title: "Restore", message: "Restore action tapped.")
|
|
showSimpleAlert(title: "Restore", message: "Restore action tapped.")
|
|
|
case .rateUs:
|
|
case .rateUs:
|
|
|
- // Replace with your App Store URL when ready.
|
|
|
|
|
- showSimpleAlert(title: "Rate Us", message: "Rate Us tapped (add App Store URL).")
|
|
|
|
|
|
|
+ settingsPopover?.performClose(nil)
|
|
|
|
|
+ settingsPopover = nil
|
|
|
|
|
+ // Replace with your App Store product URL when the app is listed.
|
|
|
|
|
+ if let url = URL(string: "https://apps.apple.com/app/id0000000000") {
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
case .support:
|
|
case .support:
|
|
|
- showSimpleAlert(title: "Support", message: "Support tapped (add support email / page).")
|
|
|
|
|
|
|
+ settingsPopover?.performClose(nil)
|
|
|
|
|
+ settingsPopover = nil
|
|
|
|
|
+ if let url = URL(string: "https://support.google.com/meet") {
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
case .moreApps:
|
|
case .moreApps:
|
|
|
- showSimpleAlert(title: "More Apps", message: "More Apps tapped (add developer page URL).")
|
|
|
|
|
|
|
+ settingsPopover?.performClose(nil)
|
|
|
|
|
+ settingsPopover = nil
|
|
|
|
|
+ // Replace with your App Store developer page URL.
|
|
|
|
|
+ if let url = URL(string: "https://apps.apple.com/developer/id0000000000") {
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ }
|
|
|
case .shareApp:
|
|
case .shareApp:
|
|
|
let urlString = "https://example.com"
|
|
let urlString = "https://example.com"
|
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.clearContents()
|
|
@@ -368,6 +417,15 @@ private extension ViewController {
|
|
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
guard let view = sender.view else { return }
|
|
guard let view = sender.view else { return }
|
|
|
let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
|
|
let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
|
|
|
|
|
+ let map: [String: String] = [
|
|
|
|
|
+ "Privacy Policy": "https://policies.google.com/privacy",
|
|
|
|
|
+ "Support": "https://support.google.com/meet",
|
|
|
|
|
+ "Terms of Services": "https://policies.google.com/terms"
|
|
|
|
|
+ ]
|
|
|
|
|
+ if let urlString = map[text], let url = URL(string: urlString) {
|
|
|
|
|
+ openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -427,6 +485,8 @@ private extension ViewController {
|
|
|
built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.")
|
|
built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.")
|
|
|
case .settings:
|
|
case .settings:
|
|
|
built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.")
|
|
built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.")
|
|
|
|
|
+ case .browse:
|
|
|
|
|
+ built = makeBrowseWebContent()
|
|
|
}
|
|
}
|
|
|
pageCache[page] = built
|
|
pageCache[page] = built
|
|
|
return built
|
|
return built
|
|
@@ -450,6 +510,104 @@ private extension ViewController {
|
|
|
return panel
|
|
return panel
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ func makeBrowseWebContent() -> NSView {
|
|
|
|
|
+ let panel = NSView()
|
|
|
|
|
+ panel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let titleLabel = textLabel("Browse the web", font: typography.pageTitle, color: palette.textPrimary)
|
|
|
|
|
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ let sub = textLabel(
|
|
|
|
|
+ "Open sites in the in-app browser (back, forward, reload, address bar). OAuth and “Continue in browser” flows stay inside the app.",
|
|
|
|
|
+ font: typography.fieldLabel,
|
|
|
|
|
+ color: palette.textSecondary
|
|
|
|
|
+ )
|
|
|
|
|
+ sub.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ sub.maximumNumberOfLines = 0
|
|
|
|
|
+ sub.lineBreakMode = .byWordWrapping
|
|
|
|
|
+
|
|
|
|
|
+ let fieldShell = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
|
|
|
|
|
+ fieldShell.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ fieldShell.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
|
|
+ styleSurface(fieldShell, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
|
|
+
|
|
|
|
|
+ let field = NSTextField(string: "")
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.isEditable = true
|
|
|
|
|
+ field.isBordered = false
|
|
|
|
|
+ field.drawsBackground = false
|
|
|
|
|
+ field.focusRingType = .none
|
|
|
|
|
+ field.font = NSFont.systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
+ field.textColor = palette.textPrimary
|
|
|
|
|
+ field.placeholderString = "https://example.com or example.com"
|
|
|
|
|
+ field.delegate = self
|
|
|
|
|
+ browseAddressField = field
|
|
|
|
|
+ fieldShell.addSubview(field)
|
|
|
|
|
+
|
|
|
|
|
+ let openBtn = meetActionButton(
|
|
|
|
|
+ title: "Open in app browser",
|
|
|
|
|
+ color: palette.primaryBlue,
|
|
|
|
|
+ textColor: .white,
|
|
|
|
|
+ width: 220,
|
|
|
|
|
+ action: #selector(browseOpenAddressClicked(_:))
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ let quickTitle = textLabel("Quick links", font: typography.joinWithURLTitle, color: palette.textPrimary)
|
|
|
|
|
+ quickTitle.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let quickRow = NSStackView()
|
|
|
|
|
+ quickRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ quickRow.orientation = .horizontal
|
|
|
|
|
+ quickRow.spacing = 10
|
|
|
|
|
+ quickRow.addArrangedSubview(browseQuickLinkButton(title: "Google Meet", action: #selector(browseQuickLinkMeetClicked(_:))))
|
|
|
|
|
+ quickRow.addArrangedSubview(browseQuickLinkButton(title: "Meet help", action: #selector(browseQuickLinkMeetHelpClicked(_:))))
|
|
|
|
|
+ quickRow.addArrangedSubview(browseQuickLinkButton(title: "Zoom help", action: #selector(browseQuickLinkZoomHelpClicked(_:))))
|
|
|
|
|
+
|
|
|
|
|
+ panel.addSubview(titleLabel)
|
|
|
|
|
+ panel.addSubview(sub)
|
|
|
|
|
+ panel.addSubview(fieldShell)
|
|
|
|
|
+ panel.addSubview(openBtn)
|
|
|
|
|
+ panel.addSubview(quickTitle)
|
|
|
|
|
+ panel.addSubview(quickRow)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
|
|
|
|
|
+ titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26),
|
|
|
|
|
+ titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: panel.trailingAnchor, constant: -28),
|
|
|
|
|
+
|
|
|
|
|
+ sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
|
|
+ sub.trailingAnchor.constraint(lessThanOrEqualTo: panel.trailingAnchor, constant: -28),
|
|
|
|
|
+ sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
|
|
|
|
|
+
|
|
|
|
|
+ fieldShell.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
|
|
+ fieldShell.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
|
|
|
+ fieldShell.topAnchor.constraint(equalTo: sub.bottomAnchor, constant: 18),
|
|
|
|
|
+
|
|
|
|
|
+ field.leadingAnchor.constraint(equalTo: fieldShell.leadingAnchor, constant: 12),
|
|
|
|
|
+ field.trailingAnchor.constraint(equalTo: fieldShell.trailingAnchor, constant: -12),
|
|
|
|
|
+ field.centerYAnchor.constraint(equalTo: fieldShell.centerYAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ openBtn.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
|
|
+ openBtn.topAnchor.constraint(equalTo: fieldShell.bottomAnchor, constant: 12),
|
|
|
|
|
+ openBtn.heightAnchor.constraint(equalToConstant: 36),
|
|
|
|
|
+
|
|
|
|
|
+ quickTitle.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
|
|
+ quickTitle.topAnchor.constraint(equalTo: openBtn.bottomAnchor, constant: 28),
|
|
|
|
|
+
|
|
|
|
|
+ quickRow.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
|
|
+ quickRow.topAnchor.constraint(equalTo: quickTitle.bottomAnchor, constant: 10)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ return panel
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func browseQuickLinkButton(title: String, action: Selector) -> NSButton {
|
|
|
|
|
+ let b = NSButton(title: title, target: self, action: action)
|
|
|
|
|
+ b.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ b.bezelStyle = .rounded
|
|
|
|
|
+ b.font = NSFont.systemFont(ofSize: 13, weight: .medium)
|
|
|
|
|
+ return b
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func applyWindowTitle(for page: SidebarPage) {
|
|
private func applyWindowTitle(for page: SidebarPage) {
|
|
|
let title: String
|
|
let title: String
|
|
|
switch page {
|
|
switch page {
|
|
@@ -463,6 +621,8 @@ private extension ViewController {
|
|
|
title = "Tutorials"
|
|
title = "Tutorials"
|
|
|
case .settings:
|
|
case .settings:
|
|
|
title = "Settings"
|
|
title = "Settings"
|
|
|
|
|
+ case .browse:
|
|
|
|
|
+ title = "Browse"
|
|
|
}
|
|
}
|
|
|
view.window?.title = title
|
|
view.window?.title = title
|
|
|
centeredTitleLabel?.stringValue = title
|
|
centeredTitleLabel?.stringValue = title
|
|
@@ -502,7 +662,7 @@ private extension ViewController {
|
|
|
private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
|
|
private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
|
|
|
switch page {
|
|
switch page {
|
|
|
case .photo, .tutorials: return false
|
|
case .photo, .tutorials: return false
|
|
|
- case .joinMeetings, .video, .settings: return true
|
|
|
|
|
|
|
+ case .joinMeetings, .video, .settings, .browse: return true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -549,6 +709,9 @@ private extension ViewController {
|
|
|
let tutorialsRow = sidebarItem("Tutorials", icon: "", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0)
|
|
let tutorialsRow = sidebarItem("Tutorials", icon: "", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0)
|
|
|
menuStack.addArrangedSubview(tutorialsRow)
|
|
menuStack.addArrangedSubview(tutorialsRow)
|
|
|
sidebarRowViews[.tutorials] = tutorialsRow
|
|
sidebarRowViews[.tutorials] = tutorialsRow
|
|
|
|
|
+ let browseRow = sidebarItem("Browse", icon: "", page: .browse)
|
|
|
|
|
+ menuStack.addArrangedSubview(browseRow)
|
|
|
|
|
+ sidebarRowViews[.browse] = browseRow
|
|
|
let settingsRow = sidebarItem("Settings", icon: "", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true)
|
|
let settingsRow = sidebarItem("Settings", icon: "", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true)
|
|
|
menuStack.addArrangedSubview(settingsRow)
|
|
menuStack.addArrangedSubview(settingsRow)
|
|
|
sidebarRowViews[.settings] = settingsRow
|
|
sidebarRowViews[.settings] = settingsRow
|
|
@@ -1540,6 +1703,16 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+extension ViewController: NSTextFieldDelegate {
|
|
|
|
|
+ func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
|
|
|
+ if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
|
|
|
|
+ browseOpenAddressClicked(nil)
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
|
private class RowHitTestView: NSView {
|
|
private class RowHitTestView: NSView {
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
@@ -2090,8 +2263,53 @@ private struct Typography {
|
|
|
let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// MARK: - In-app browser (macOS WKWebView + chrome)
|
|
|
|
|
+// Note: This target is AppKit/macOS. iOS would use WKWebView or SFSafariViewController; Android would use WebView or Custom Tabs.
|
|
|
|
|
+
|
|
|
|
|
+private enum InAppBrowserURLPolicy: Equatable {
|
|
|
|
|
+ case allowAll
|
|
|
|
|
+ case whitelist(hostSuffixes: [String])
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private func inAppBrowserURLAllowed(_ url: URL, policy: InAppBrowserURLPolicy) -> Bool {
|
|
|
|
|
+ let scheme = (url.scheme ?? "").lowercased()
|
|
|
|
|
+ if scheme == "about" { return true }
|
|
|
|
|
+ guard scheme == "http" || scheme == "https" else { return false }
|
|
|
|
|
+ guard let host = url.host?.lowercased() else { return false }
|
|
|
|
|
+ switch policy {
|
|
|
|
|
+ case .allowAll:
|
|
|
|
|
+ return true
|
|
|
|
|
+ case .whitelist(let suffixes):
|
|
|
|
|
+ for suffix in suffixes {
|
|
|
|
|
+ let s = suffix.lowercased()
|
|
|
|
|
+ if host == s || host.hasSuffix("." + s) { return true }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private enum InAppBrowserWebKitSupport {
|
|
|
|
|
+ static let sharedProcessPool = WKProcessPool()
|
|
|
|
|
+
|
|
|
|
|
+ static func makeWebViewConfiguration() -> WKWebViewConfiguration {
|
|
|
|
|
+ let config = WKWebViewConfiguration()
|
|
|
|
|
+ config.processPool = sharedProcessPool
|
|
|
|
|
+ config.websiteDataStore = .default()
|
|
|
|
|
+ config.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
|
|
+ if #available(macOS 12.3, *) {
|
|
|
|
|
+ config.preferences.isElementFullscreenEnabled = true
|
|
|
|
|
+ }
|
|
|
|
|
+ config.mediaTypesRequiringUserActionForPlayback = []
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ config.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
|
|
|
+ }
|
|
|
|
|
+ config.applicationNameForUserAgent = "MeetingsApp/1.0"
|
|
|
|
|
+ return config
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private final class InAppBrowserWindowController: NSWindowController {
|
|
private final class InAppBrowserWindowController: NSWindowController {
|
|
|
- private let browserViewController = InAppBrowserViewController()
|
|
|
|
|
|
|
+ private let browserViewController = InAppBrowserContainerViewController()
|
|
|
|
|
|
|
|
init() {
|
|
init() {
|
|
|
let browserWindow = NSWindow(
|
|
let browserWindow = NSWindow(
|
|
@@ -2100,7 +2318,7 @@ private final class InAppBrowserWindowController: NSWindowController {
|
|
|
backing: .buffered,
|
|
backing: .buffered,
|
|
|
defer: false
|
|
defer: false
|
|
|
)
|
|
)
|
|
|
- browserWindow.title = "Google Meet"
|
|
|
|
|
|
|
+ browserWindow.title = "Browser"
|
|
|
browserWindow.center()
|
|
browserWindow.center()
|
|
|
browserWindow.contentViewController = browserViewController
|
|
browserWindow.contentViewController = browserViewController
|
|
|
super.init(window: browserWindow)
|
|
super.init(window: browserWindow)
|
|
@@ -2111,24 +2329,315 @@ private final class InAppBrowserWindowController: NSWindowController {
|
|
|
nil
|
|
nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- func load(url: URL) {
|
|
|
|
|
|
|
+ func load(url: URL, policy: InAppBrowserURLPolicy) {
|
|
|
|
|
+ browserViewController.setNavigationPolicy(policy)
|
|
|
browserViewController.load(url: url)
|
|
browserViewController.load(url: url)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-private final class InAppBrowserViewController: NSViewController, WKNavigationDelegate {
|
|
|
|
|
- private let webView = WKWebView(frame: .zero)
|
|
|
|
|
|
|
+private final class InAppBrowserContainerViewController: NSViewController, WKNavigationDelegate, WKUIDelegate, NSTextFieldDelegate {
|
|
|
|
|
+ private var webView: WKWebView!
|
|
|
|
|
+ private var webContainerView: NSView!
|
|
|
|
|
+ private weak var urlField: NSTextField?
|
|
|
|
|
+ private var backButton: NSButton!
|
|
|
|
|
+ private var forwardButton: NSButton!
|
|
|
|
|
+ private var reloadStopButton: NSButton!
|
|
|
|
|
+ private var goButton: NSButton!
|
|
|
|
|
+ private var progressBar: NSProgressIndicator!
|
|
|
|
|
+
|
|
|
private var lastLoadedURL: URL?
|
|
private var lastLoadedURL: URL?
|
|
|
|
|
+ private var navigationPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
|
|
+ private var processTerminateRetryCount = 0
|
|
|
|
|
+ /// Includes fresh WKWebView instances so each retry gets a new WebContent process after a crash.
|
|
|
|
|
+ private let maxProcessTerminateRetries = 3
|
|
|
|
|
+ private var kvoTokens: [NSKeyValueObservation] = []
|
|
|
|
|
+
|
|
|
|
|
+ deinit {
|
|
|
|
|
+ kvoTokens.removeAll()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setNavigationPolicy(_ policy: InAppBrowserURLPolicy) {
|
|
|
|
|
+ navigationPolicy = policy
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
override func loadView() {
|
|
override func loadView() {
|
|
|
|
|
+ let root = NSView()
|
|
|
|
|
+ root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let wv = makeWebView()
|
|
|
|
|
+ webView = wv
|
|
|
|
|
+
|
|
|
|
|
+ let webHost = NSView()
|
|
|
|
|
+ webHost.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ webHost.wantsLayer = true
|
|
|
|
|
+ webHost.addSubview(wv)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ wv.leadingAnchor.constraint(equalTo: webHost.leadingAnchor),
|
|
|
|
|
+ wv.trailingAnchor.constraint(equalTo: webHost.trailingAnchor),
|
|
|
|
|
+ wv.topAnchor.constraint(equalTo: webHost.topAnchor),
|
|
|
|
|
+ wv.bottomAnchor.constraint(equalTo: webHost.bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ webContainerView = webHost
|
|
|
|
|
+
|
|
|
|
|
+ let toolbar = NSStackView()
|
|
|
|
|
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ toolbar.orientation = .horizontal
|
|
|
|
|
+ toolbar.spacing = 8
|
|
|
|
|
+ toolbar.alignment = .centerY
|
|
|
|
|
+ toolbar.edgeInsets = NSEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
|
|
|
|
|
+
|
|
|
|
|
+ backButton = makeToolbarButton(title: "◀", symbolName: "chevron.backward", accessibilityDescription: "Back")
|
|
|
|
|
+ backButton.target = self
|
|
|
|
|
+ backButton.action = #selector(goBack)
|
|
|
|
|
+ forwardButton = makeToolbarButton(title: "▶", symbolName: "chevron.forward", accessibilityDescription: "Forward")
|
|
|
|
|
+ forwardButton.target = self
|
|
|
|
|
+ forwardButton.action = #selector(goForward)
|
|
|
|
|
+ reloadStopButton = makeToolbarButton(title: "Reload", symbolName: "arrow.clockwise", accessibilityDescription: "Reload")
|
|
|
|
|
+ reloadStopButton.target = self
|
|
|
|
|
+ reloadStopButton.action = #selector(reloadOrStop)
|
|
|
|
|
+
|
|
|
|
|
+ let field = NSTextField(string: "")
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
|
|
+ field.placeholderString = "Address"
|
|
|
|
|
+ field.cell?.sendsActionOnEndEditing = false
|
|
|
|
|
+ field.delegate = self
|
|
|
|
|
+ urlField = field
|
|
|
|
|
+
|
|
|
|
|
+ goButton = NSButton(title: "Go", target: self, action: #selector(addressFieldSubmitted))
|
|
|
|
|
+ goButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ goButton.bezelStyle = .rounded
|
|
|
|
|
+
|
|
|
|
|
+ toolbar.addArrangedSubview(backButton)
|
|
|
|
|
+ toolbar.addArrangedSubview(forwardButton)
|
|
|
|
|
+ toolbar.addArrangedSubview(reloadStopButton)
|
|
|
|
|
+ toolbar.addArrangedSubview(field)
|
|
|
|
|
+ toolbar.addArrangedSubview(goButton)
|
|
|
|
|
+ field.widthAnchor.constraint(greaterThanOrEqualToConstant: 240).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let bar = NSProgressIndicator()
|
|
|
|
|
+ bar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ bar.style = .bar
|
|
|
|
|
+ bar.isIndeterminate = false
|
|
|
|
|
+ bar.minValue = 0
|
|
|
|
|
+ bar.maxValue = 1
|
|
|
|
|
+ bar.doubleValue = 0
|
|
|
|
|
+ bar.isHidden = true
|
|
|
|
|
+ progressBar = bar
|
|
|
|
|
+
|
|
|
|
|
+ let separator = NSBox()
|
|
|
|
|
+ separator.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ separator.boxType = .separator
|
|
|
|
|
+
|
|
|
webView.navigationDelegate = self
|
|
webView.navigationDelegate = self
|
|
|
- webView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- view = webView
|
|
|
|
|
|
|
+ webView.uiDelegate = self
|
|
|
|
|
+
|
|
|
|
|
+ root.addSubview(toolbar)
|
|
|
|
|
+ root.addSubview(bar)
|
|
|
|
|
+ root.addSubview(separator)
|
|
|
|
|
+ root.addSubview(webHost)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ toolbar.leadingAnchor.constraint(equalTo: root.leadingAnchor),
|
|
|
|
|
+ toolbar.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
|
|
|
|
+ toolbar.topAnchor.constraint(equalTo: root.topAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ bar.leadingAnchor.constraint(equalTo: root.leadingAnchor),
|
|
|
|
|
+ bar.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
|
|
|
|
+ bar.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
|
|
|
|
|
+ bar.heightAnchor.constraint(equalToConstant: 3),
|
|
|
|
|
+
|
|
|
|
|
+ separator.leadingAnchor.constraint(equalTo: root.leadingAnchor),
|
|
|
|
|
+ separator.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
|
|
|
|
+ separator.topAnchor.constraint(equalTo: bar.bottomAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ webHost.leadingAnchor.constraint(equalTo: root.leadingAnchor),
|
|
|
|
|
+ webHost.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
|
|
|
|
+ webHost.topAnchor.constraint(equalTo: separator.bottomAnchor),
|
|
|
|
|
+ webHost.bottomAnchor.constraint(equalTo: root.bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ view = root
|
|
|
|
|
+ installWebViewObservers()
|
|
|
|
|
+ syncToolbarFromWebView()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func makeWebView() -> WKWebView {
|
|
|
|
|
+ let wv = WKWebView(frame: .zero, configuration: InAppBrowserWebKitSupport.makeWebViewConfiguration())
|
|
|
|
|
+ wv.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ return wv
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func teardownWebViewObservers() {
|
|
|
|
|
+ kvoTokens.removeAll()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// New `WKWebView` = new WebContent process (helps after GPU/JS crashes on heavy sites like Meet).
|
|
|
|
|
+ private func replaceWebViewAndLoad(url: URL) {
|
|
|
|
|
+ teardownWebViewObservers()
|
|
|
|
|
+ webView.navigationDelegate = nil
|
|
|
|
|
+ webView.uiDelegate = nil
|
|
|
|
|
+ webView.removeFromSuperview()
|
|
|
|
|
+
|
|
|
|
|
+ let wv = makeWebView()
|
|
|
|
|
+ webView = wv
|
|
|
|
|
+ webContainerView.addSubview(wv)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ wv.leadingAnchor.constraint(equalTo: webContainerView.leadingAnchor),
|
|
|
|
|
+ wv.trailingAnchor.constraint(equalTo: webContainerView.trailingAnchor),
|
|
|
|
|
+ wv.topAnchor.constraint(equalTo: webContainerView.topAnchor),
|
|
|
|
|
+ wv.bottomAnchor.constraint(equalTo: webContainerView.bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ webView.navigationDelegate = self
|
|
|
|
|
+ webView.uiDelegate = self
|
|
|
|
|
+ installWebViewObservers()
|
|
|
|
|
+ syncToolbarFromWebView()
|
|
|
|
|
+ webView.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func makeToolbarButton(title: String, symbolName: String, accessibilityDescription: String) -> NSButton {
|
|
|
|
|
+ let b = NSButton()
|
|
|
|
|
+ b.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ b.bezelStyle = .texturedRounded
|
|
|
|
|
+ b.setAccessibilityLabel(accessibilityDescription)
|
|
|
|
|
+ if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityDescription) {
|
|
|
|
|
+ b.image = img
|
|
|
|
|
+ b.imagePosition = .imageOnly
|
|
|
|
|
+ } else {
|
|
|
|
|
+ b.title = title
|
|
|
|
|
+ }
|
|
|
|
|
+ b.widthAnchor.constraint(greaterThanOrEqualToConstant: 32).isActive = true
|
|
|
|
|
+ return b
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func installWebViewObservers() {
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.canGoBack, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncToolbarFromWebView()
|
|
|
|
|
+ })
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.canGoForward, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncToolbarFromWebView()
|
|
|
|
|
+ })
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.isLoading, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncToolbarFromWebView()
|
|
|
|
|
+ })
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.estimatedProgress, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncProgressFromWebView()
|
|
|
|
|
+ })
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.title, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncWindowTitle()
|
|
|
|
|
+ })
|
|
|
|
|
+ kvoTokens.append(webView.observe(\.url, options: [.new]) { [weak self] _, _ in
|
|
|
|
|
+ self?.syncAddressFieldFromWebView()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func syncToolbarFromWebView() {
|
|
|
|
|
+ backButton?.isEnabled = webView.canGoBack
|
|
|
|
|
+ forwardButton?.isEnabled = webView.canGoForward
|
|
|
|
|
+ if webView.isLoading {
|
|
|
|
|
+ if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Stop") {
|
|
|
|
|
+ reloadStopButton.image = img
|
|
|
|
|
+ reloadStopButton.imagePosition = .imageOnly
|
|
|
|
|
+ reloadStopButton.title = ""
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reloadStopButton.title = "Stop"
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reload") {
|
|
|
|
|
+ reloadStopButton.image = img
|
|
|
|
|
+ reloadStopButton.imagePosition = .imageOnly
|
|
|
|
|
+ reloadStopButton.title = ""
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reloadStopButton.title = "Reload"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ syncProgressFromWebView()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func syncProgressFromWebView() {
|
|
|
|
|
+ guard let progressBar else { return }
|
|
|
|
|
+ if webView.isLoading {
|
|
|
|
|
+ progressBar.isHidden = false
|
|
|
|
|
+ progressBar.doubleValue = webView.estimatedProgress
|
|
|
|
|
+ } else {
|
|
|
|
|
+ progressBar.isHidden = true
|
|
|
|
|
+ progressBar.doubleValue = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func syncAddressFieldFromWebView() {
|
|
|
|
|
+ guard let urlField, urlField.currentEditor() == nil, let url = webView.url else { return }
|
|
|
|
|
+ urlField.stringValue = url.absoluteString
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func syncWindowTitle() {
|
|
|
|
|
+ let t = webView.title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
+ let host = webView.url?.host ?? ""
|
|
|
|
|
+ view.window?.title = t.isEmpty ? (host.isEmpty ? "Browser" : host) : t
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func load(url: URL) {
|
|
func load(url: URL) {
|
|
|
lastLoadedURL = url
|
|
lastLoadedURL = url
|
|
|
|
|
+ processTerminateRetryCount = 0
|
|
|
|
|
+ urlField?.stringValue = url.absoluteString
|
|
|
webView.load(URLRequest(url: url))
|
|
webView.load(URLRequest(url: url))
|
|
|
|
|
+ syncWindowTitle()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func goBack() {
|
|
|
|
|
+ webView.goBack()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func goForward() {
|
|
|
|
|
+ webView.goForward()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func reloadOrStop() {
|
|
|
|
|
+ if webView.isLoading {
|
|
|
|
|
+ webView.stopLoading()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ webView.reload()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func addressFieldSubmitted() {
|
|
|
|
|
+ let raw = urlField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
+ guard raw.isEmpty == false else { return }
|
|
|
|
|
+ var normalized = raw
|
|
|
|
|
+ if normalized.lowercased().hasPrefix("http://") == false && normalized.lowercased().hasPrefix("https://") == false {
|
|
|
|
|
+ normalized = "https://\(normalized)"
|
|
|
|
|
+ }
|
|
|
|
|
+ guard let url = URL(string: normalized),
|
|
|
|
|
+ let scheme = url.scheme?.lowercased(),
|
|
|
|
|
+ scheme == "http" || scheme == "https",
|
|
|
|
|
+ url.host != nil
|
|
|
|
|
+ else {
|
|
|
|
|
+ let alert = NSAlert()
|
|
|
|
|
+ alert.messageText = "Invalid address"
|
|
|
|
|
+ alert.informativeText = "Enter a valid web address, for example https://example.com"
|
|
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
|
|
+ alert.runModal()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ guard inAppBrowserURLAllowed(url, policy: navigationPolicy) else {
|
|
|
|
|
+ presentBlockedHostAlert()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ load(url: url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func presentBlockedHostAlert() {
|
|
|
|
|
+ let alert = NSAlert()
|
|
|
|
|
+ alert.messageText = "Address not allowed"
|
|
|
|
|
+ alert.informativeText = "This URL is not permitted with the current in-app browser policy (whitelist)."
|
|
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
|
|
+ alert.runModal()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
|
|
+ processTerminateRetryCount = 0
|
|
|
|
|
+ syncAddressFieldFromWebView()
|
|
|
|
|
+ syncWindowTitle()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
@@ -2137,19 +2646,85 @@ private final class InAppBrowserViewController: NSViewController, WKNavigationDe
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
let alert = NSAlert()
|
|
let alert = NSAlert()
|
|
|
- alert.messageText = "Unable to Open Link"
|
|
|
|
|
- alert.informativeText = "Could not load this page in the in-app browser.\n\n\(error.localizedDescription)\n\nOpen in default browser instead?"
|
|
|
|
|
- alert.addButton(withTitle: "Open in Browser")
|
|
|
|
|
- alert.addButton(withTitle: "Cancel")
|
|
|
|
|
- let result = alert.runModal()
|
|
|
|
|
- if result == .alertFirstButtonReturn, let url = lastLoadedURL {
|
|
|
|
|
- NSWorkspace.shared.open(url)
|
|
|
|
|
|
|
+ alert.messageText = "Unable to load page"
|
|
|
|
|
+ alert.informativeText = "Could not load this page in the in-app browser.\n\n\(error.localizedDescription)"
|
|
|
|
|
+ alert.addButton(withTitle: "Try Again")
|
|
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
|
|
+ if alert.runModal() == .alertFirstButtonReturn, let url = lastLoadedURL {
|
|
|
|
|
+ processTerminateRetryCount = 0
|
|
|
|
|
+ webView.load(URLRequest(url: url))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
|
- // WebKit process can be terminated by the OS; retry current page once.
|
|
|
|
|
- webView.reload()
|
|
|
|
|
|
|
+ guard let url = lastLoadedURL else { return }
|
|
|
|
|
+
|
|
|
|
|
+ if processTerminateRetryCount < maxProcessTerminateRetries {
|
|
|
|
|
+ processTerminateRetryCount += 1
|
|
|
|
|
+ replaceWebViewAndLoad(url: url)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let alert = NSAlert()
|
|
|
|
|
+ alert.messageText = "Page stopped loading"
|
|
|
|
|
+ alert.informativeText =
|
|
|
|
|
+ "The in-app browser closed this page unexpectedly. You can try loading it again in this same window."
|
|
|
|
|
+ alert.addButton(withTitle: "Try Again")
|
|
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
|
|
+ if alert.runModal() == .alertFirstButtonReturn {
|
|
|
|
|
+ processTerminateRetryCount = 0
|
|
|
|
|
+ replaceWebViewAndLoad(url: url)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func webView(
|
|
|
|
|
+ _ webView: WKWebView,
|
|
|
|
|
+ decidePolicyFor navigationAction: WKNavigationAction,
|
|
|
|
|
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
|
|
|
|
+ ) {
|
|
|
|
|
+ guard let url = navigationAction.request.url else {
|
|
|
|
|
+ decisionHandler(.allow)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ let scheme = (url.scheme ?? "").lowercased()
|
|
|
|
|
+ if scheme == "mailto" || scheme == "tel" {
|
|
|
|
|
+ decisionHandler(.cancel)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if inAppBrowserURLAllowed(url, policy: navigationPolicy) == false {
|
|
|
|
|
+ if navigationAction.targetFrame?.isMainFrame != false {
|
|
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
|
|
+ self?.presentBlockedHostAlert()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ decisionHandler(.cancel)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ decisionHandler(.allow)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func webView(
|
|
|
|
|
+ _ webView: WKWebView,
|
|
|
|
|
+ createWebViewWith configuration: WKWebViewConfiguration,
|
|
|
|
|
+ for navigationAction: WKNavigationAction,
|
|
|
|
|
+ windowFeatures: WKWindowFeatures
|
|
|
|
|
+ ) -> WKWebView? {
|
|
|
|
|
+ if navigationAction.targetFrame == nil, let requestURL = navigationAction.request.url {
|
|
|
|
|
+ if inAppBrowserURLAllowed(requestURL, policy: navigationPolicy) {
|
|
|
|
|
+ webView.load(URLRequest(url: requestURL))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ presentBlockedHostAlert()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
|
|
|
+ if control === urlField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
|
|
|
|
+ addressFieldSubmitted()
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|