|
|
@@ -0,0 +1,188 @@
|
|
|
+//
|
|
|
+// IndeedJobBrowserWindowController.swift
|
|
|
+// App for Indeed
|
|
|
+//
|
|
|
+
|
|
|
+import Cocoa
|
|
|
+import WebKit
|
|
|
+
|
|
|
+/// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
|
|
|
+final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
|
|
|
+ /// When set, a leading **Done** control calls this so the host can hide the embedded browser (same-window UX).
|
|
|
+ var onDismissEmbedded: (() -> Void)?
|
|
|
+ private let webView: WKWebView = {
|
|
|
+ let configuration = WKWebViewConfiguration()
|
|
|
+ configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
+ return WKWebView(frame: .zero, configuration: configuration)
|
|
|
+ }()
|
|
|
+
|
|
|
+ private var pendingURL: URL?
|
|
|
+
|
|
|
+ private let backButton = NSButton()
|
|
|
+ private let forwardButton = NSButton()
|
|
|
+ private let reloadButton = NSButton()
|
|
|
+ private let openExternallyButton = NSButton(title: "Open in Browser", target: nil, action: nil)
|
|
|
+ private let dismissEmbeddedButton = NSButton(title: "Done", target: nil, action: nil)
|
|
|
+
|
|
|
+ override func loadView() {
|
|
|
+ view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
|
|
|
+ }
|
|
|
+
|
|
|
+ override func viewDidLoad() {
|
|
|
+ super.viewDidLoad()
|
|
|
+ webView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ webView.navigationDelegate = self
|
|
|
+ webView.uiDelegate = self
|
|
|
+ webView.customUserAgent = Self.desktopSafariLikeUserAgent
|
|
|
+
|
|
|
+ configureToolbarButton(backButton, symbolName: "chevron.backward", action: #selector(goBack))
|
|
|
+ configureToolbarButton(forwardButton, symbolName: "chevron.forward", action: #selector(goForward))
|
|
|
+ configureToolbarButton(reloadButton, symbolName: "arrow.clockwise", action: #selector(reload))
|
|
|
+
|
|
|
+ openExternallyButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ openExternallyButton.bezelStyle = .rounded
|
|
|
+ openExternallyButton.isBordered = true
|
|
|
+ openExternallyButton.target = self
|
|
|
+ openExternallyButton.action = #selector(openInDefaultBrowser)
|
|
|
+ openExternallyButton.toolTip = "Open this page in your default web browser"
|
|
|
+
|
|
|
+ dismissEmbeddedButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ dismissEmbeddedButton.bezelStyle = .rounded
|
|
|
+ dismissEmbeddedButton.isBordered = true
|
|
|
+ dismissEmbeddedButton.target = self
|
|
|
+ dismissEmbeddedButton.action = #selector(dismissEmbedded)
|
|
|
+ dismissEmbeddedButton.toolTip = "Return to the previous screen"
|
|
|
+
|
|
|
+ let toolbar = NSView()
|
|
|
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ toolbar.wantsLayer = true
|
|
|
+ toolbar.layer?.backgroundColor = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1).cgColor
|
|
|
+
|
|
|
+ let barStack: NSStackView
|
|
|
+ if onDismissEmbedded != nil {
|
|
|
+ barStack = NSStackView(views: [dismissEmbeddedButton, backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
|
|
|
+ } else {
|
|
|
+ barStack = NSStackView(views: [backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
|
|
|
+ }
|
|
|
+ barStack.orientation = .horizontal
|
|
|
+ barStack.spacing = 8
|
|
|
+ barStack.alignment = .centerY
|
|
|
+ barStack.distribution = .fill
|
|
|
+ barStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ toolbar.addSubview(barStack)
|
|
|
+ view.addSubview(toolbar)
|
|
|
+ view.addSubview(webView)
|
|
|
+
|
|
|
+ var layoutConstraints: [NSLayoutConstraint] = [
|
|
|
+ toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
+ toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
+ toolbar.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
+ toolbar.heightAnchor.constraint(equalToConstant: 48),
|
|
|
+
|
|
|
+ barStack.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor, constant: 12),
|
|
|
+ barStack.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor, constant: -12),
|
|
|
+ barStack.centerYAnchor.constraint(equalTo: toolbar.centerYAnchor),
|
|
|
+
|
|
|
+ backButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
+ backButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
+ forwardButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
+ forwardButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
+ reloadButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
+ reloadButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
+
|
|
|
+ webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
+ webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
+ webView.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
|
|
|
+ webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
|
+ ]
|
|
|
+ if onDismissEmbedded != nil {
|
|
|
+ layoutConstraints.append(dismissEmbeddedButton.heightAnchor.constraint(equalToConstant: 28))
|
|
|
+ }
|
|
|
+ NSLayoutConstraint.activate(layoutConstraints)
|
|
|
+
|
|
|
+ updateNavigationButtons()
|
|
|
+
|
|
|
+ if let pendingURL {
|
|
|
+ webView.load(URLRequest(url: pendingURL))
|
|
|
+ self.pendingURL = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func loadPage(_ url: URL) {
|
|
|
+ if isViewLoaded {
|
|
|
+ webView.load(URLRequest(url: url))
|
|
|
+ } else {
|
|
|
+ pendingURL = url
|
|
|
+ }
|
|
|
+ updateNavigationButtons()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Adds this controller as a child of `parent` and pins `view` to `host` (used by the dashboard main panel).
|
|
|
+ func embed(in host: NSView, parent: NSViewController) {
|
|
|
+ parent.addChild(self)
|
|
|
+ view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ host.addSubview(view)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ view.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
|
|
+ view.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
|
|
+ view.topAnchor.constraint(equalTo: host.topAnchor),
|
|
|
+ view.bottomAnchor.constraint(equalTo: host.bottomAnchor)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ private func configureToolbarButton(_ button: NSButton, symbolName: String, action: Selector) {
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.bezelStyle = .texturedRounded
|
|
|
+ button.isBordered = true
|
|
|
+ button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
|
|
|
+ button.imagePosition = .imageOnly
|
|
|
+ button.target = self
|
|
|
+ button.action = action
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateNavigationButtons() {
|
|
|
+ backButton.isEnabled = webView.canGoBack
|
|
|
+ forwardButton.isEnabled = webView.canGoForward
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func goBack() {
|
|
|
+ webView.goBack()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func goForward() {
|
|
|
+ webView.goForward()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func reload() {
|
|
|
+ webView.reload()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func openInDefaultBrowser() {
|
|
|
+ guard let url = webView.url else { return }
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func dismissEmbedded() {
|
|
|
+ onDismissEmbedded?()
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
+ updateNavigationButtons()
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
|
+ updateNavigationButtons()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
|
|
|
+ func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
|
+ if navigationAction.targetFrame == nil {
|
|
|
+ webView.load(navigationAction.request)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
|
|
|
+ private static let desktopSafariLikeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
|
|
|
+}
|