Нет описания

IndeedJobBrowserWindowController.swift 7.1KB

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