Rename to rossler\\attix
This commit is contained in:
108
App/Browser View/BrowserView.swift
Normal file
108
App/Browser View/BrowserView.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// BrowserView.swift
|
||||
// SBrowser
|
||||
//
|
||||
// Created by James Magahern on 7/21/20.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
class BrowserView: UIView
|
||||
{
|
||||
let titlebarView = TitlebarView()
|
||||
|
||||
var toolbarView: ToolbarView? {
|
||||
didSet { addSubview(toolbarView!) }
|
||||
}
|
||||
|
||||
var webView: WKWebView? {
|
||||
didSet {
|
||||
oldValue?.removeFromSuperview()
|
||||
|
||||
if let toolbarView = toolbarView {
|
||||
insertSubview(webView!, belowSubview: toolbarView)
|
||||
} else {
|
||||
addSubview(webView!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var keyboardWillShowObserver: AnyCancellable?
|
||||
var keyboardWillHideObserver: AnyCancellable?
|
||||
var keyboardLayoutOffset: CGFloat = 0 { didSet { setNeedsLayout() } }
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
|
||||
addSubview(titlebarView)
|
||||
|
||||
keyboardWillShowObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification).sink { [weak self] notification in
|
||||
self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
}
|
||||
|
||||
keyboardWillHideObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification).sink { [weak self] notification in
|
||||
self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
}
|
||||
}
|
||||
|
||||
private func adjustOffsetForKeyboardNotification(userInfo: [AnyHashable : Any]) {
|
||||
guard let keyboardEndFrame = userInfo[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
guard let animationDuration = userInfo[UIWindow.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { return }
|
||||
guard let animationCurve = userInfo[UIWindow.keyboardAnimationCurveUserInfoKey] as? Int else { return }
|
||||
|
||||
let animationOptions: UIView.AnimationOptions = { curve -> UIView.AnimationOptions in
|
||||
switch UIView.AnimationCurve(rawValue: curve) {
|
||||
case .easeIn: return .curveEaseIn
|
||||
case .easeOut: return .curveEaseOut
|
||||
case .easeInOut: return .curveEaseInOut
|
||||
default: return .init()
|
||||
}
|
||||
}(animationCurve)
|
||||
|
||||
keyboardLayoutOffset = bounds.height - keyboardEndFrame.minY
|
||||
UIView.animate(withDuration: animationDuration, delay: 0.0, options: animationOptions, animations: { self.layoutIfNeeded() }, completion: nil)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
var webViewContentInset = UIEdgeInsets()
|
||||
|
||||
bringSubviewToFront(titlebarView)
|
||||
|
||||
var titlebarHeight: CGFloat = 24.0
|
||||
titlebarHeight += safeAreaInsets.top
|
||||
titlebarView.frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: titlebarHeight))
|
||||
webViewContentInset.top += titlebarView.frame.height
|
||||
|
||||
if let toolbarView = toolbarView {
|
||||
var toolbarSize = toolbarView.sizeThatFits(bounds.size)
|
||||
|
||||
// Compact: toolbar is at the bottom
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
var bottomOffset: CGFloat = 0.0
|
||||
if keyboardLayoutOffset == 0 {
|
||||
toolbarSize.height += safeAreaInsets.bottom
|
||||
} else if toolbarView.urlBar?.textField.isFirstResponder ?? false {
|
||||
bottomOffset = keyboardLayoutOffset
|
||||
}
|
||||
|
||||
toolbarView.bounds = CGRect(origin: .zero, size: toolbarSize)
|
||||
toolbarView.center = CGPoint(x: bounds.center.x, y: bounds.maxY - (toolbarView.bounds.height / 2) - bottomOffset)
|
||||
webViewContentInset.bottom += toolbarView.frame.height
|
||||
} else {
|
||||
// Regular: toolbar is at the top
|
||||
toolbarView.frame = CGRect(origin: CGPoint(x: 0.0, y: titlebarView.frame.maxY), size: toolbarSize)
|
||||
webViewContentInset.top += toolbarView.frame.height
|
||||
}
|
||||
}
|
||||
|
||||
// Fix web view content insets
|
||||
if let webView = webView {
|
||||
webView.scrollView.layer.masksToBounds = false // allow content to draw under titlebar/toolbar
|
||||
webView.frame = bounds.inset(by: webViewContentInset)
|
||||
}
|
||||
}
|
||||
}
|
||||
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