From 3cedd3f387688529662997d3543bb4eac1f2615a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 14 Dec 2021 18:50:33 -0800 Subject: [PATCH] Redirect Rules: implements redirect rules UI --- App/Backend/PersonalRedirectRules.swift | 24 --- ...BrowserViewController+WebKitDelegate.swift | 2 +- App/Browser View/BrowserViewController.swift | 2 - App/Common UI/GenericContentView.swift | 15 +- .../RedirectRulesSettingsViewController.swift | 163 +++++++++++++++++- App/Settings/Settings.swift | 27 +++ SBrowser.xcodeproj/project.pbxproj | 4 - 7 files changed, 198 insertions(+), 39 deletions(-) delete mode 100644 App/Backend/PersonalRedirectRules.swift diff --git a/App/Backend/PersonalRedirectRules.swift b/App/Backend/PersonalRedirectRules.swift deleted file mode 100644 index d4ff038..0000000 --- a/App/Backend/PersonalRedirectRules.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PersonalRedirectRules.swift -// App -// -// Created by James Magahern on 9/30/20. -// - -import Foundation - -class PersonalRedirectRules -{ - func redirectedURL(for url: URL) -> URL? { - if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - - // Rule for nitter.net - if url.host == "twitter.com" { - components.host = "nitter.net" - return components.url - } - } - - return nil - } -} diff --git a/App/Browser View/BrowserViewController+WebKitDelegate.swift b/App/Browser View/BrowserViewController+WebKitDelegate.swift index fff3b03..8bafa63 100644 --- a/App/Browser View/BrowserViewController+WebKitDelegate.swift +++ b/App/Browser View/BrowserViewController+WebKitDelegate.swift @@ -97,7 +97,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate preferences.allowsContentJavaScript = allowJavaScript if let url = navigationAction.request.url, - let redirectedURL = redirectRules.redirectedURL(for: url) + let redirectedURL = Settings.shared.redirectRule(for: url) { tab.beginLoadingURL(redirectedURL) decisionHandler(.cancel, preferences) diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index 01d43fe..431a452 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -22,8 +22,6 @@ class BrowserViewController: UIViewController internal let findOnPageController = FindOnPageViewController() internal let autocompleteViewController = AutocompleteViewController() - internal let redirectRules = PersonalRedirectRules() - internal var policyManager: ResourcePolicyManager { tabController.policyManager } override var canBecomeFirstResponder: Bool { true } diff --git a/App/Common UI/GenericContentView.swift b/App/Common UI/GenericContentView.swift index 53b5e0a..70ba517 100644 --- a/App/Common UI/GenericContentView.swift +++ b/App/Common UI/GenericContentView.swift @@ -29,17 +29,22 @@ class GenericContentView : UIView, UIContentView self.applicator = applicator super.init(frame: .zero) addSubview(view) + + view.translatesAutoresizingMaskIntoConstraints = false + + let guide = layoutMarginsGuide + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: guide.leadingAnchor), + view.trailingAnchor.constraint(equalTo: guide.trailingAnchor), + view.topAnchor.constraint(equalTo: guide.topAnchor), + view.bottomAnchor.constraint(equalTo: guide.bottomAnchor), + ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func layoutSubviews() { - super.layoutSubviews() - view.frame = bounds.inset(by: insets) - } - internal func apply(_ configuration: Configuration) { applicator(configuration, view) } diff --git a/App/Settings/RedirectRulesSettingsViewController.swift b/App/Settings/RedirectRulesSettingsViewController.swift index b6eafe9..b7d9e0a 100644 --- a/App/Settings/RedirectRulesSettingsViewController.swift +++ b/App/Settings/RedirectRulesSettingsViewController.swift @@ -7,10 +7,52 @@ import UIKit -class RedirectRulesSettingsViewController: UIViewController +class RedirectRulesSettingsViewController: UICollectionViewController { + private enum Section { + case rules + case addButton + } + + private enum Items: String { + case addButton + } + + private lazy var dataSource: UICollectionViewDiffableDataSource = { + let registry = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in + var config = cell.defaultContentConfiguration() + if item == Items.addButton.rawValue { + config.text = "Add Redirect Rule" + config.textProperties.font = .preferredFont(forTextStyle: .subheadline) + config.textProperties.alignment = .center + config.image = .init(systemName: "plus.circle") + + cell.accessories = [] + } else { + if let rule = Settings.shared.redirectRules[item] { + config.text = "\(item) → \(rule)" + } + + let deleteOptions = UICellAccessory.DeleteOptions(isHidden: false, reservedLayoutWidth: nil, tintColor: .red, backgroundColor: nil) + cell.accessories = [ + .delete(displayed: .always, options: deleteOptions, actionHandler: { [unowned self] in + deleteRedirectRule(fromHost: item) + }) + ] + } + + cell.contentConfiguration = config + } + + return UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, itemIdentifier) in + collectionView.dequeueConfiguredReusableCell(using: registry, for: indexPath, item: itemIdentifier) + } + }() + init() { - super.init(nibName: nil, bundle: nil) + let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + let layout = UICollectionViewCompositionalLayout.list(using: config) + super.init(collectionViewLayout: layout) tabBarItem.title = "Redirect Rules" tabBarItem.image = UIImage(systemName: "arrowshape.zigzag.forward") @@ -21,7 +63,122 @@ class RedirectRulesSettingsViewController: UIViewController } override func viewDidLoad() { - view.backgroundColor = .red + var snapshot = dataSource.snapshot() + snapshot.appendSections([ .rules, .addButton ]) + snapshot.appendItems(Settings.shared.redirectRules.keys.sorted(), toSection: .rules) + snapshot.appendItems([ Items.addButton.rawValue ], toSection: .addButton) + dataSource.applySnapshotUsingReloadData(snapshot) + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = dataSource.itemIdentifier(for: indexPath) + if item == Items.addButton.rawValue { + let createRuleViewController = CreateRedirectRuleViewController() + createRuleViewController.finishedCreatingRule = { [unowned self] (fromHost, toHost) in + if fromHost.count > 0 && toHost.count > 0 { + addRedirectRule(fromHost: fromHost, toHost: toHost) + } + } + + let navController = UINavigationController(rootViewController: createRuleViewController) + present(navController, animated: true, completion: nil) + } + } + + private func addRedirectRule(fromHost: String, toHost: String) { + Settings.shared.redirectRules[fromHost] = toHost + + var snapshot = dataSource.snapshot() + snapshot.appendItems([ fromHost ], toSection: .rules) + dataSource.apply(snapshot, animatingDifferences: true) + } + + private func deleteRedirectRule(fromHost: String) { + Settings.shared.redirectRules.removeValue(forKey: fromHost) + + var snapshot = dataSource.snapshot() + snapshot.deleteItems([ fromHost ]) + dataSource.apply(snapshot, animatingDifferences: true) + } +} + +class CreateRedirectRuleViewController: UICollectionViewController +{ + enum Section { + case hosts + } + + enum Item: String { + case fromHost + case toHost + } + + struct TextFieldCellConfiguration: UIContentConfiguration { + var textField: UITextField + + func makeContentView() -> UIView & UIContentView { + return GenericContentView(configuration: self, view: textField) { _,_ in } + } + + func updated(for state: UIConfigurationState) -> TextFieldCellConfiguration { + self + } + } + + public var finishedCreatingRule: ((_ fromHost: String, _ toHost: String) -> Void)? + + private let fromHostField = UITextField(frame: .zero) + private let toHostField = UITextField(frame: .zero) + + private let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: nil, menu: nil) + + private lazy var dataSource: UICollectionViewDiffableDataSource = { + let registry = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in + if item == Item.fromHost { + cell.contentConfiguration = TextFieldCellConfiguration(textField: fromHostField) + } else if item == Item.toHost { + cell.contentConfiguration = TextFieldCellConfiguration(textField: toHostField) + } + } + + return UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, itemIdentifier) in + collectionView.dequeueConfiguredReusableCell(using: registry, for: indexPath, item: itemIdentifier) + } + }() + + init() { + let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + let layout = UICollectionViewCompositionalLayout.list(using: config) + super.init(collectionViewLayout: layout) + + preferredContentSize = CGSize(width: -1, height: 320.0) + + self.title = "Add Redirect Rule" + + navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { [unowned self] _ in + dismiss(animated: true, completion: nil) + }, menu: nil) + + doneButton.primaryAction = UIAction { [unowned self] _ in + finishedCreatingRule?(fromHostField.text!, toHostField.text!) + dismiss(animated: true, completion: nil) + } + + navigationItem.rightBarButtonItem = doneButton + + fromHostField.placeholder = "From Host" + toHostField.placeholder = "To Host" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + var snapshot = dataSource.snapshot() + snapshot.appendSections([ Section.hosts ]) + snapshot.appendItems([ Item.fromHost, Item.toHost ], toSection: .hosts) + dataSource.applySnapshotUsingReloadData(snapshot) } } diff --git a/App/Settings/Settings.swift b/App/Settings/Settings.swift index 6ef4b9c..29df91f 100644 --- a/App/Settings/Settings.swift +++ b/App/Settings/Settings.swift @@ -28,6 +28,19 @@ public struct SettingProperty } } +extension Dictionary: RawRepresentable where Key == String, Value == String { + public typealias RawValue = [String: String] + + public init?(rawValue: [String : String]) { + self.init() + self.merge(rawValue, uniquingKeysWith: { $1 }) + } + + public var rawValue: [String : String] { + return self + } +} + class Settings { static let shared = Settings() @@ -50,4 +63,18 @@ class Settings @SettingProperty(key: "searchProvider") public var searchProvider: SearchProviderSetting = .searxnor + + @SettingProperty(key: "redirectRules") + public var redirectRules: [String: String] = [:] + + func redirectRule(for url: URL) -> URL? { + if var components = URLComponents(url: url, resolvingAgainstBaseURL: false), let host = url.host { + if let alternateHost = redirectRules[host] { + components.host = alternateHost + return components.url + } + } + + return nil + } } diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index 6ded038..9aa870b 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB88EFC24D3BA560006F850 /* TabController.swift */; }; 1AB88EFF24D3BBA50006F850 /* TabPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB88EFE24D3BBA50006F850 /* TabPickerViewController.swift */; }; 1AB88F0624D4D3A90006F850 /* UIGestureRecognizer+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB88F0524D4D3A90006F850 /* UIGestureRecognizer+Actions.swift */; }; - 1AD3103D252541E600A4A952 /* PersonalRedirectRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD3103C252541E600A4A952 /* PersonalRedirectRules.swift */; }; 1AD31040252545BF00A4A952 /* FindOnPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD3103F252545BF00A4A952 /* FindOnPageView.swift */; }; 1AD3104325254FB900A4A952 /* FindOnPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD3104225254FB900A4A952 /* FindOnPageViewController.swift */; }; 1AD310452525586B00A4A952 /* DocumentControlItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD310442525586B00A4A952 /* DocumentControlItemView.swift */; }; @@ -112,7 +111,6 @@ 1AB88EFC24D3BA560006F850 /* TabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabController.swift; sourceTree = ""; }; 1AB88EFE24D3BBA50006F850 /* TabPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPickerViewController.swift; sourceTree = ""; }; 1AB88F0524D4D3A90006F850 /* UIGestureRecognizer+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Actions.swift"; sourceTree = ""; }; - 1AD3103C252541E600A4A952 /* PersonalRedirectRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalRedirectRules.swift; sourceTree = ""; }; 1AD3103F252545BF00A4A952 /* FindOnPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindOnPageView.swift; sourceTree = ""; }; 1AD3104225254FB900A4A952 /* FindOnPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindOnPageViewController.swift; sourceTree = ""; }; 1AD310442525586B00A4A952 /* DocumentControlItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentControlItemView.swift; sourceTree = ""; }; @@ -328,7 +326,6 @@ children = ( CD853BD224E77BEF00D2BDCC /* History */, 1ADFF4AD24C8ED32006DC7AE /* ResourcePolicyManager.swift */, - 1AD3103C252541E600A4A952 /* PersonalRedirectRules.swift */, ); path = Backend; sourceTree = ""; @@ -593,7 +590,6 @@ CDD0522425F8055700DD1771 /* SearchProvider.swift in Sources */, CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */, 1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */, - 1AD3103D252541E600A4A952 /* PersonalRedirectRules.swift in Sources */, 1AB88F0624D4D3A90006F850 /* UIGestureRecognizer+Actions.swift in Sources */, CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */, 1ADFF46224C7DE53006DC7AE /* SceneDelegate.swift in Sources */,