Rename to rossler\\attix
This commit is contained in:
340
App/Browser View/BrowserViewController.swift
Normal file
340
App/Browser View/BrowserViewController.swift
Normal file
@@ -0,0 +1,340 @@
|
||||
//
|
||||
// BrowserViewController.swift
|
||||
// SBrowser
|
||||
//
|
||||
// Created by James Magahern on 7/21/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class BrowserViewController: UIViewController, WKNavigationDelegate,
|
||||
UITextFieldDelegate, ScriptPolicyViewControllerDelegate,
|
||||
UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate
|
||||
{
|
||||
let browserView = BrowserView()
|
||||
var tab: Tab { didSet { didChangeTab(tab) } }
|
||||
var webView: WKWebView { tab.webView }
|
||||
|
||||
private let tabController = TabController()
|
||||
private let toolbarController = ToolbarViewController()
|
||||
|
||||
private var policyManager: ResourcePolicyManager { tabController.policyManager }
|
||||
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
private var titleObservation: NSKeyValueObservation?
|
||||
private var loadingObservation: NSKeyValueObservation?
|
||||
private var backButtonObservation: NSKeyValueObservation?
|
||||
private var forwardButtonObservation: NSKeyValueObservation?
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
|
||||
|
||||
init() {
|
||||
self.tab = tabController.tabs.first!
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
addChild(toolbarController)
|
||||
didChangeTab(tab)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func loadView() {
|
||||
browserView.toolbarView = toolbarController.toolbarView
|
||||
|
||||
// Refresh button
|
||||
toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { [unowned self] action in
|
||||
if self.webView.isLoading {
|
||||
self.webView.stopLoading()
|
||||
} else {
|
||||
self.webView.reload()
|
||||
}
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Back button
|
||||
toolbarController.backButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
self.webView.goBack()
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Forward button
|
||||
toolbarController.forwardButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
self.webView.goForward()
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Share button
|
||||
toolbarController.shareButton.addAction(UIAction(handler: { [unowned self, toolbarController] _ in
|
||||
if let url = self.webView.url {
|
||||
let itemProvider = NSItemProvider(item: url as NSURL, typeIdentifier: UTType.url.identifier)
|
||||
let config = UIActivityItemsConfiguration(itemProviders: [ itemProvider ])
|
||||
config.metadataProvider = { metadataKey in
|
||||
switch metadataKey {
|
||||
case .title: return self.webView.title
|
||||
case .messageBody: return self.webView.title
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
config.previewProvider = { index, intent, suggestedSize in
|
||||
NSItemProvider(item: self.tab.favicon, typeIdentifier: UTType.image.identifier)
|
||||
}
|
||||
|
||||
let activityController = UIActivityViewController(activityItemsConfiguration: config)
|
||||
activityController.popoverPresentationController?.sourceView = toolbarController.shareButton
|
||||
self.present(activityController, animated: true, completion: nil)
|
||||
}
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Script button
|
||||
toolbarController.scriptControllerIconView.addAction(UIAction(handler: { [unowned self] action in
|
||||
let hostOrigin = self.webView.url?.host ?? ""
|
||||
let loadedScripts = self.tab.allowedScriptOrigins.union(self.tab.blockedScriptOrigins)
|
||||
let scriptViewController = ScriptPolicyViewController(policyManager: self.policyManager,
|
||||
hostOrigin: hostOrigin,
|
||||
loadedScripts: loadedScripts,
|
||||
scriptsAllowedForTab: self.tab.javaScriptEnabled)
|
||||
scriptViewController.delegate = self
|
||||
|
||||
let navController = UINavigationController(rootViewController: scriptViewController)
|
||||
navController.modalPresentationStyle = .popover
|
||||
navController.popoverPresentationController?.sourceView = self.toolbarController.scriptControllerIconView
|
||||
navController.popoverPresentationController?.delegate = self
|
||||
|
||||
self.present(navController, animated: true, completion: nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Dark mode button
|
||||
toolbarController.darkModeButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
self.tab.bridge.darkModeEnabled = !self.tab.bridge.darkModeEnabled
|
||||
self.toolbarController.darkModeEnabled = self.tab.bridge.darkModeEnabled
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Tabs button
|
||||
toolbarController.windowButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
let tabPickerController = TabPickerViewController(tabController: self.tabController)
|
||||
tabPickerController.delegate = self
|
||||
tabPickerController.selectedTab = self.tab
|
||||
|
||||
let navController = UINavigationController(rootViewController: tabPickerController)
|
||||
navController.modalPresentationStyle = .popover
|
||||
navController.popoverPresentationController?.sourceView = self.toolbarController.windowButton
|
||||
navController.popoverPresentationController?.delegate = self
|
||||
|
||||
self.present(navController, animated: true, completion: nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
let newTabAction = UIAction { [unowned self] action in
|
||||
if let gestureRecognizer = action.sender as? UILongPressGestureRecognizer {
|
||||
if gestureRecognizer.state != .began { return }
|
||||
}
|
||||
|
||||
// Create new tab
|
||||
let newTab = tabController.createNewTab(url: nil)
|
||||
self.tab = newTab
|
||||
}
|
||||
|
||||
let gestureRecognizer = UILongPressGestureRecognizer(action: newTabAction)
|
||||
toolbarController.windowButton.addGestureRecognizer(gestureRecognizer)
|
||||
|
||||
// New tab button
|
||||
toolbarController.newTabButton.addAction(newTabAction, for: .touchUpInside)
|
||||
|
||||
// TextField delegate
|
||||
toolbarController.urlBar.textField.delegate = self
|
||||
|
||||
self.view = browserView
|
||||
}
|
||||
|
||||
private func updateLoadProgress(forWebView webView: WKWebView) {
|
||||
if webView.estimatedProgress == 1.0 {
|
||||
toolbarController.urlBar.loadProgress = .complete
|
||||
} else {
|
||||
toolbarController.urlBar.loadProgress = .loading(progress: webView.estimatedProgress)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTitleAndURL(forWebView webView: WKWebView) {
|
||||
browserView.titlebarView.titleLabelView.text = webView.title
|
||||
|
||||
if let urlString = webView.url?.absoluteString {
|
||||
toolbarController.urlBar.textField.text = urlString
|
||||
} else {
|
||||
toolbarController.urlBar.textField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private func didChangeTab(_ tab: Tab) {
|
||||
tab.delegate = self
|
||||
|
||||
let webView = tab.webView
|
||||
webView.allowsBackForwardNavigationGestures = true
|
||||
webView.navigationDelegate = self
|
||||
|
||||
// Change webView
|
||||
browserView.webView = webView
|
||||
|
||||
// Load progress
|
||||
updateLoadProgress(forWebView: webView)
|
||||
loadingObservation = webView.observe(\.estimatedProgress) { [unowned self] (webView, observedChange) in
|
||||
self.updateLoadProgress(forWebView: webView)
|
||||
}
|
||||
|
||||
// Title observer
|
||||
updateTitleAndURL(forWebView: webView)
|
||||
titleObservation = webView.observe(\.title, changeHandler: { [unowned self] (webView, observedChange) in
|
||||
self.updateTitleAndURL(forWebView: webView)
|
||||
})
|
||||
|
||||
// Back/forward observer
|
||||
toolbarController.backButton.isEnabled = webView.canGoBack
|
||||
backButtonObservation = webView.observe(\.canGoBack, changeHandler: { [toolbarController] (webView, observedChange) in
|
||||
toolbarController.backButton.isEnabled = webView.canGoBack
|
||||
})
|
||||
|
||||
toolbarController.forwardButton.isEnabled = webView.canGoForward
|
||||
forwardButtonObservation = webView.observe(\.canGoForward, changeHandler: { [toolbarController] (webView, observedChange) in
|
||||
toolbarController.forwardButton.isEnabled = webView.canGoForward
|
||||
})
|
||||
|
||||
// Script blocker button
|
||||
updateScriptBlockerButton()
|
||||
|
||||
// Enforce dark mode setting
|
||||
tab.bridge.darkModeEnabled = toolbarController.darkModeEnabled
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
// Not sure why this doesn't happen automatically...
|
||||
toolbarController.traitCollectionDidChange(previousTraitCollection)
|
||||
}
|
||||
|
||||
private func updateScriptBlockerButton() {
|
||||
var numBlockedScripts: Int = tab.blockedScriptOrigins.count
|
||||
if tab.url != nil, tab.javaScriptEnabled == false {
|
||||
// Because the page is blocked too, notify.
|
||||
numBlockedScripts += 1
|
||||
}
|
||||
|
||||
var scriptsAllowedForHost = false
|
||||
if let url = webView.url, let host = url.host, policyManager.allowedOriginsForScriptResources().contains(host) {
|
||||
scriptsAllowedForHost = true
|
||||
}
|
||||
|
||||
let iconView = toolbarController.scriptControllerIconView
|
||||
iconView.shieldsDown = tab.javaScriptEnabled
|
||||
iconView.someScriptsAllowed = scriptsAllowedForHost
|
||||
iconView.setBlockedScriptsNumber(numBlockedScripts)
|
||||
}
|
||||
|
||||
// MARK: UIPopoverPresentationControllerDelegate
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
// Forces popovers to present on iPhone
|
||||
return .none
|
||||
}
|
||||
|
||||
// MARK: Navigation Delegate
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
// Reset tracking this
|
||||
tab.allowedScriptOrigins.removeAll()
|
||||
tab.blockedScriptOrigins.removeAll()
|
||||
updateScriptBlockerButton()
|
||||
|
||||
updateTitleAndURL(forWebView: webView)
|
||||
|
||||
// Start requesting favicon
|
||||
if let url = webView.url {
|
||||
tab.updateFaviconForURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
toolbarController.urlBar.loadProgress = .complete
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void)
|
||||
{
|
||||
var allowJavaScript = tab.javaScriptEnabled
|
||||
if !allowJavaScript, let host = navigationAction.request.url?.host {
|
||||
// Check origin policy
|
||||
allowJavaScript = policyManager.allowedOriginsForScriptResources().contains(host)
|
||||
}
|
||||
|
||||
preferences.allowsContentJavaScript = allowJavaScript
|
||||
decisionHandler(.allow, preferences)
|
||||
}
|
||||
|
||||
// MARK: UITextField Delegate
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if let text = textField.text?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) {
|
||||
|
||||
// Dumb rules for stuff that "looks like" a URL
|
||||
if !text.contains(" "),
|
||||
text.components(separatedBy: ".").count > 1,
|
||||
var url = URL(string: text)
|
||||
{
|
||||
if url.scheme == nil {
|
||||
let urlString = "https://\(text)"
|
||||
if let fixedURL = URL(string: urlString) {
|
||||
url = fixedURL
|
||||
}
|
||||
}
|
||||
|
||||
tab.beginLoadingURL(url)
|
||||
} else {
|
||||
// Assume google search
|
||||
let queryString = text.replacingOccurrences(of: " ", with: "+")
|
||||
let searchURL = URL(string: "https://google.com/search?q=\(queryString)&gbv=1")! // gbv=1: no JS
|
||||
tab.beginLoadingURL(searchURL)
|
||||
}
|
||||
|
||||
textField.resignFirstResponder()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Tab Delegate
|
||||
|
||||
func didBlockScriptOrigin(_ origin: String, forTab: Tab) {
|
||||
updateScriptBlockerButton()
|
||||
}
|
||||
|
||||
// MARK: Tab Picker Delegate
|
||||
|
||||
func tabPicker(_ picker: TabPickerViewController, didSelectTab tab: Tab) {
|
||||
self.tab = tab
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func tabPicker(_ picker: TabPickerViewController, willCloseTab tab: Tab) {
|
||||
// If closed tab is current tab, pick another one.
|
||||
if tab == self.tab {
|
||||
if let nextTab = tabController.tabs.last(where: { $0 != tab }) {
|
||||
self.tab = nextTab
|
||||
}
|
||||
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Script Policy View Controller Delegate
|
||||
|
||||
func didChangeScriptPolicy() {
|
||||
tab.bridge.policyDataSourceDidChange()
|
||||
webView.reload()
|
||||
}
|
||||
|
||||
func setScriptsEnabledForTab(_ enabled: Bool) {
|
||||
tab.javaScriptEnabled = enabled
|
||||
toolbarController.scriptControllerIconView.shieldsDown = enabled
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user