Нема описа

IndeedJobBrowserWindowController.swift 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. //
  2. // IndeedJobBrowserWindowController.swift
  3. // App for Indeed
  4. //
  5. import Cocoa
  6. import WebKit
  7. /// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
  8. final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
  9. /// When set, a leading **Done** control calls this so the host can hide the embedded browser (same-window UX).
  10. var onDismissEmbedded: (() -> Void)?
  11. private let webView: WKWebView = {
  12. let configuration = WKWebViewConfiguration()
  13. configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
  14. return WKWebView(frame: .zero, configuration: configuration)
  15. }()
  16. private var pendingURL: URL?
  17. private let backButton = NSButton()
  18. private let forwardButton = NSButton()
  19. private let reloadButton = NSButton()
  20. private let openExternallyButton = NSButton(title: "Open in Browser", target: nil, action: nil)
  21. private let dismissEmbeddedButton = NSButton(title: "Done", target: nil, action: nil)
  22. override func loadView() {
  23. view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
  24. }
  25. override func viewDidLoad() {
  26. super.viewDidLoad()
  27. webView.translatesAutoresizingMaskIntoConstraints = false
  28. webView.navigationDelegate = self
  29. webView.uiDelegate = self
  30. webView.customUserAgent = Self.desktopSafariLikeUserAgent
  31. configureToolbarButton(backButton, symbolName: "chevron.backward", action: #selector(goBack))
  32. configureToolbarButton(forwardButton, symbolName: "chevron.forward", action: #selector(goForward))
  33. configureToolbarButton(reloadButton, symbolName: "arrow.clockwise", action: #selector(reload))
  34. openExternallyButton.translatesAutoresizingMaskIntoConstraints = false
  35. openExternallyButton.bezelStyle = .rounded
  36. openExternallyButton.isBordered = true
  37. openExternallyButton.target = self
  38. openExternallyButton.action = #selector(openInDefaultBrowser)
  39. openExternallyButton.toolTip = "Open this page in your default web browser"
  40. dismissEmbeddedButton.translatesAutoresizingMaskIntoConstraints = false
  41. dismissEmbeddedButton.bezelStyle = .rounded
  42. dismissEmbeddedButton.isBordered = true
  43. dismissEmbeddedButton.target = self
  44. dismissEmbeddedButton.action = #selector(dismissEmbedded)
  45. dismissEmbeddedButton.toolTip = "Return to the previous screen"
  46. let toolbar = NSView()
  47. toolbar.translatesAutoresizingMaskIntoConstraints = false
  48. toolbar.wantsLayer = true
  49. toolbar.layer?.backgroundColor = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1).cgColor
  50. let barStack: NSStackView
  51. if onDismissEmbedded != nil {
  52. barStack = NSStackView(views: [dismissEmbeddedButton, backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
  53. } else {
  54. barStack = NSStackView(views: [backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
  55. }
  56. barStack.orientation = .horizontal
  57. barStack.spacing = 8
  58. barStack.alignment = .centerY
  59. barStack.distribution = .fill
  60. barStack.translatesAutoresizingMaskIntoConstraints = false
  61. toolbar.addSubview(barStack)
  62. view.addSubview(toolbar)
  63. view.addSubview(webView)
  64. var layoutConstraints: [NSLayoutConstraint] = [
  65. toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  66. toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  67. toolbar.topAnchor.constraint(equalTo: view.topAnchor),
  68. toolbar.heightAnchor.constraint(equalToConstant: 48),
  69. barStack.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor, constant: 12),
  70. barStack.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor, constant: -12),
  71. barStack.centerYAnchor.constraint(equalTo: toolbar.centerYAnchor),
  72. backButton.widthAnchor.constraint(equalToConstant: 32),
  73. backButton.heightAnchor.constraint(equalToConstant: 28),
  74. forwardButton.widthAnchor.constraint(equalToConstant: 32),
  75. forwardButton.heightAnchor.constraint(equalToConstant: 28),
  76. reloadButton.widthAnchor.constraint(equalToConstant: 32),
  77. reloadButton.heightAnchor.constraint(equalToConstant: 28),
  78. webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  79. webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  80. webView.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
  81. webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  82. ]
  83. if onDismissEmbedded != nil {
  84. layoutConstraints.append(dismissEmbeddedButton.heightAnchor.constraint(equalToConstant: 28))
  85. }
  86. NSLayoutConstraint.activate(layoutConstraints)
  87. updateNavigationButtons()
  88. if let pendingURL {
  89. webView.load(URLRequest(url: pendingURL))
  90. self.pendingURL = nil
  91. }
  92. }
  93. func loadPage(_ url: URL) {
  94. if isViewLoaded {
  95. webView.load(URLRequest(url: url))
  96. } else {
  97. pendingURL = url
  98. }
  99. updateNavigationButtons()
  100. }
  101. /// Adds this controller as a child of `parent` and pins `view` to `host` (used by the dashboard main panel).
  102. func embed(in host: NSView, parent: NSViewController) {
  103. parent.addChild(self)
  104. view.translatesAutoresizingMaskIntoConstraints = false
  105. host.addSubview(view)
  106. NSLayoutConstraint.activate([
  107. view.leadingAnchor.constraint(equalTo: host.leadingAnchor),
  108. view.trailingAnchor.constraint(equalTo: host.trailingAnchor),
  109. view.topAnchor.constraint(equalTo: host.topAnchor),
  110. view.bottomAnchor.constraint(equalTo: host.bottomAnchor)
  111. ])
  112. }
  113. private func configureToolbarButton(_ button: NSButton, symbolName: String, action: Selector) {
  114. button.translatesAutoresizingMaskIntoConstraints = false
  115. button.bezelStyle = .texturedRounded
  116. button.isBordered = true
  117. button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
  118. button.imagePosition = .imageOnly
  119. button.target = self
  120. button.action = action
  121. }
  122. private func updateNavigationButtons() {
  123. backButton.isEnabled = webView.canGoBack
  124. forwardButton.isEnabled = webView.canGoForward
  125. }
  126. @objc private func goBack() {
  127. webView.goBack()
  128. }
  129. @objc private func goForward() {
  130. webView.goForward()
  131. }
  132. @objc private func reload() {
  133. webView.reload()
  134. }
  135. @objc private func openInDefaultBrowser() {
  136. guard let url = webView.url else { return }
  137. NSWorkspace.shared.open(url)
  138. }
  139. @objc private func dismissEmbedded() {
  140. onDismissEmbedded?()
  141. }
  142. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  143. updateNavigationButtons()
  144. }
  145. func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
  146. updateNavigationButtons()
  147. }
  148. /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
  149. func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
  150. if navigationAction.targetFrame == nil {
  151. webView.load(navigationAction.request)
  152. }
  153. return nil
  154. }
  155. /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
  156. 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"
  157. }