diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index c5cef7a..2988efe 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -795,7 +795,7 @@ extension BrowserViewController: UITextFieldDelegate currentTab.beginLoadingURL(url) } else { - let searchURL = Settings.shared.searchProvider.provider().searchURLWithQuery(text) + let searchURL = Settings.shared.currentSearchProvider().searchURLWithQuery(text) currentTab.beginLoadingURL(searchURL) } diff --git a/App/Settings/AddSearchEngineViewController.swift b/App/Settings/AddSearchEngineViewController.swift new file mode 100644 index 0000000..aea1bc9 --- /dev/null +++ b/App/Settings/AddSearchEngineViewController.swift @@ -0,0 +1,74 @@ +// +// AddSearchEngineViewController.swift +// App +// +// Created by James Magahern on 9/29/25. +// + +import SwiftUI +import UIKit + +@MainActor +struct AddSearchEngineView: View +{ + @State var model: ViewModel + + var body: some View { + Form { + Section { + HStack { + Text("Name") + TextField("Name", text: $model.name) + .multilineTextAlignment(.trailing) + .autocorrectionDisabled() + } + } + + Section { + HStack { + Text("URL") + TextField("https://example.com/query=%q", text: $model.url) + .multilineTextAlignment(.trailing) + .autocorrectionDisabled() + .textContentType(.none) + } + } footer: { + Text("URL must contain %q, which will be replaced by the query. ") + } + + Section { + Toggle("Set as default", isOn: $model.makeDefault) + } + } + .navigationTitle("Add Search Engine") + } + + @MainActor + @Observable + class ViewModel + { + var name: String = "" + var url: String = "" + var makeDefault: Bool = false + } +} + +@MainActor +class AddSearchEngineViewController: UIHostingController +{ + public var viewModel: AddSearchEngineView.ViewModel + + init() { + self.viewModel = AddSearchEngineView.ViewModel() + super.init(rootView: AddSearchEngineView(model: viewModel)) + } + + @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#Preview { + @Previewable @State var model = AddSearchEngineView.ViewModel() + AddSearchEngineView(model: model) +} diff --git a/App/Settings/Amber/AmberSettingsView.swift b/App/Settings/Amber/AmberSettingsView.swift index c0d080d..5c9014d 100644 --- a/App/Settings/Amber/AmberSettingsView.swift +++ b/App/Settings/Amber/AmberSettingsView.swift @@ -23,8 +23,8 @@ struct AmberSettingsView: View { @Environment(\.presentationMode) @Binding private var presentationMode - @State private var searchProvider = Settings.shared.searchProvider { - didSet { Settings.shared.searchProvider = searchProvider } + @State private var defaultSearchEngineName = Settings.shared.defaultSearchEngineName { + didSet { Settings.shared.defaultSearchEngineName = defaultSearchEngineName } } var body: some View { @@ -35,12 +35,12 @@ struct AmberSettingsView: View { }) Section(header: Text("Search Provider"), content: { - ForEach(Settings.SearchProviderSetting.allCases, id: \.self, content: { setting in - Button(action: { searchProvider = setting }, label: { + ForEach(Array(Settings.shared.searchEngines.keys).sorted(), id: \.self, content: { name in + Button(action: { defaultSearchEngineName = name }, label: { HStack { - Text(setting.rawValue) + Text(name) Spacer() - if searchProvider == setting { + if defaultSearchEngineName == name { Image(systemName: "checkmark") } } diff --git a/App/Settings/GeneralSettingsViewController.swift b/App/Settings/GeneralSettingsViewController.swift index e8e96af..2bd1b75 100644 --- a/App/Settings/GeneralSettingsViewController.swift +++ b/App/Settings/GeneralSettingsViewController.swift @@ -109,55 +109,56 @@ class GeneralSettingsViewController: UIViewController case searchEngine = "Search Engine" case syncServer = "Sync Server" } - + typealias Item = String - + static let SearchProviderPopupItem = "searchProvider.popup" static let SyncServerItem = "syncServer.field" - + let dataSource: UICollectionViewDiffableDataSource let collectionView: UICollectionView - + let viewModel: ViewModel + static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout { - #if targetEnvironment(macCatalyst) - let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), - heightDimension: .fractionalHeight(1.0)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) +#if targetEnvironment(macCatalyst) + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.60), - heightDimension: .absolute(44)) - - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) - group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(1.0), top: nil, trailing: nil, bottom: nil) - - let insets = NSDirectionalEdgeInsets(top: 24.0, leading: 64.0, bottom: 24.0, trailing: 64.0) - - let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.40), - heightDimension: .estimated(44.0)) - let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: headerFooterSize, - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .topLeading, - absoluteOffset: CGPoint(x: insets.leading, y: insets.top + 5.0) - ) - sectionHeader.extendsBoundary = false - sectionHeader.contentInsets = insets + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.60), + heightDimension: .absolute(44)) - let section = NSCollectionLayoutSection(group: group) - // section.interGroupSpacing = spacing - section.contentInsets = insets - section.supplementariesFollowContentInsets = true - section.boundarySupplementaryItems = [ sectionHeader ] + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) + group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(1.0), top: nil, trailing: nil, bottom: nil) - let layout = UICollectionViewCompositionalLayout(section: section) - return layout - #else - var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - listConfiguration.headerMode = .supplementary - return UICollectionViewCompositionalLayout.list(using: listConfiguration) - #endif + let insets = NSDirectionalEdgeInsets(top: 24.0, leading: 64.0, bottom: 24.0, trailing: 64.0) + + let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.40), + heightDimension: .estimated(44.0)) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerFooterSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading, + absoluteOffset: CGPoint(x: insets.leading, y: insets.top + 5.0) + ) + sectionHeader.extendsBoundary = false + sectionHeader.contentInsets = insets + + let section = NSCollectionLayoutSection(group: group) + // section.interGroupSpacing = spacing + section.contentInsets = insets + section.supplementariesFollowContentInsets = true + section.boundarySupplementaryItems = [ sectionHeader ] + + let layout = UICollectionViewCompositionalLayout(section: section) + return layout +#else + var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + listConfiguration.headerMode = .supplementary + return UICollectionViewCompositionalLayout.list(using: listConfiguration) +#endif } - + static func sectionHeaderConfiguration(forIdiom idiom: UIUserInterfaceIdiom, sectionName: String) -> UIContentConfiguration { if idiom == .mac { return LabelContentConfiguration( @@ -166,33 +167,41 @@ class GeneralSettingsViewController: UIViewController textAlignment: .right ) } else { - var config = UIListContentConfiguration.plainHeader() + var config = UIListContentConfiguration.header() config.text = sectionName return config } } - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) static let staticIdiom = UIUserInterfaceIdiom.mac - #else +#else static let staticIdiom = UIUserInterfaceIdiom.pad - #endif - +#endif + init() { let actionHandler = { (action: UIAction) in - let providerString = action.title - let provider = Settings.SearchProviderSetting(rawValue: providerString)! - Settings.shared.searchProvider = provider + let engineName = action.title + Settings.shared.defaultSearchEngineName = engineName } - + + let viewModel = ViewModel() let itemCellRegistry = UICollectionView.CellRegistration { cell, indexPath, identifier in if identifier == Self.SearchProviderPopupItem { - let menu = UIMenu(children: Settings.SearchProviderSetting.allCases.map { provider in - let action = UIAction(title: provider.rawValue, handler: actionHandler) - action.state = Settings.shared.searchProvider == provider ? .on : .off + let names = Settings.shared.searchEngines.keys.sorted() + var engineMenuItems: [UIMenuElement] = names.map { name in + let action = UIAction(title: name, handler: actionHandler) + action.state = (Settings.shared.defaultSearchEngineName == name) ? .on : .off return action - }) - + } + + engineMenuItems.append(UIMenu(options: .displayInline, children: [ + UIAction(title: "Manage Search Engines…", handler: { _ in + viewModel.onManageSearchEngines() + }) + ])) + + let menu = UIMenu(children: engineMenuItems) cell.contentConfiguration = ButtonContentConfiguration(menu: menu) } else if identifier == Self.SyncServerItem { cell.contentConfiguration = TextFieldContentConfiguration( @@ -206,58 +215,114 @@ class GeneralSettingsViewController: UIViewController returnKeyType: .done ) } - + #if !targetEnvironment(macCatalyst) - cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() + cell.backgroundConfiguration = UIBackgroundConfiguration.listCell() #endif } - + let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader, handler: { - (cell, string, indexPath) in + (cell, string, indexPath) in let sectionName = Section.allCases[indexPath.section].rawValue cell.contentConfiguration = Self.sectionHeaderConfiguration(forIdiom: Self.staticIdiom, sectionName: sectionName) }) - + let layout = Self.createLayout(forIdiom: Self.staticIdiom) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .systemGroupedBackground - + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { - (collectionView, indexPath, identifier) in + (collectionView, indexPath, identifier) in return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistry, for: indexPath, item: identifier) } - + dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistry, for: indexPath) } - + + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) - + collectionView.delegate = self tabBarItem.title = "General" tabBarItem.image = UIImage(systemName: "gear") + + let resetPopup = { [weak self] in + guard let self else { return } + + var snapshot = dataSource.snapshot() + snapshot.reloadItems([ Self.SearchProviderPopupItem ]) + dataSource.apply(snapshot, animatingDifferences: false) + } + + viewModel.onManageSearchEngines = { [weak self] in + guard let self else { return } + + // Reset menu item (to show currently selected search engine) + resetPopup() + + let viewController = ManageSearchEnginesViewController() + viewController.title = "Manage Search Engines" + + viewController.model.onAddSearchEngine = { [weak self] in + guard let self else { return } + + let addSearchEngineViewController = AddSearchEngineViewController() + let navController = UINavigationController(rootViewController: addSearchEngineViewController) + + // Cancel + addSearchEngineViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { _ in + navController.dismiss(animated: true) + }) + + // Done + addSearchEngineViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [weak self] _ in + self?.saveCustomSearchEngine(model: addSearchEngineViewController.viewModel) + navController.dismiss(animated: true) + + resetPopup() + viewController.model.reloadConfiguredEngines() + }) + + self.present(navController, animated: true) + } + + viewController.model.onChangeEngines = { + resetPopup() + } + + #if targetEnvironment(macCatalyst) + let alertController = UIAlertController(title: "Not yet implemented for macOS.", message: nil, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in alertController.dismiss(animated: true) })) + present(alertController, animated: true) + #else + navigationController?.pushViewController(viewController, animated: true) + #endif + } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func loadView() { self.view = collectionView } - + override func viewDidLoad() { super.viewDidLoad() - - var snapshot = dataSource.snapshot() - snapshot.appendSections(Section.allCases) - // iOS - // snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine) - snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine) - snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer) - dataSource.apply(snapshot, animatingDifferences: false) + applySnapshot(animatingDifferences: false) } + // MARK: - Types + + @MainActor + @Observable + class ViewModel + { + var onManageSearchEngines: () -> Void = {} + } } extension GeneralSettingsViewController : UICollectionViewDelegate { @@ -265,3 +330,36 @@ extension GeneralSettingsViewController : UICollectionViewDelegate { false } } + +private extension GeneralSettingsViewController { + func applySnapshot(animatingDifferences: Bool) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(Section.allCases) + snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine) + snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + func saveCustomSearchEngine(model: AddSearchEngineView.ViewModel) { + let name = model.name.trimmingCharacters(in: .whitespacesAndNewlines) + let url = model.url.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty, !url.isEmpty else { return } + + // Require placeholder + guard url.contains("%q") || url.contains("%s") else { return } + + // Save + var engines = Settings.shared.searchEngines + engines[name] = url + Settings.shared.searchEngines = engines + + if model.makeDefault { + Settings.shared.defaultSearchEngineName = name + } + + // Reset inputs and refresh UI + var snapshot = dataSource.snapshot() + snapshot.reloadItems([ Self.SearchProviderPopupItem ]) + dataSource.apply(snapshot, animatingDifferences: false) + } +} diff --git a/App/Settings/ManageSearchEnginesViewController.swift b/App/Settings/ManageSearchEnginesViewController.swift new file mode 100644 index 0000000..1e9136e --- /dev/null +++ b/App/Settings/ManageSearchEnginesViewController.swift @@ -0,0 +1,139 @@ +// +// ManageSearchEnginesViewController.swift +// App +// +// Created by James Magahern on 9/29/25. +// + +import SwiftUI +import UIKit + +@MainActor +struct ManageSearchEnginesView: View +{ + @State var model: ViewModel + + var body: some View { + Form { + Section { + List($model.configuredEngines, editActions: .delete) { row in + HStack { + Image(uiImage: .checkmark) + .opacity(row.wrappedValue.isDefault ? 1.0 : 0.0) + + VStack(alignment: .leading) { + Text(row.wrappedValue.name) + .font(.body) + .bold() + + Text(row.wrappedValue.url) + .font(.caption) + } + } + .id(row.id) + + .swipeActions(edge: .leading) { + Button { model.makeDefault(row.id) } label: { + Label("Make Default", systemImage: "checkmark") + } + .tint(.blue) + } + + .swipeActions(edge: .trailing) { + Button(role: .destructive) { model.deleteEngine(row.id) } label: { + Label("Delete", systemImage: "trash") + } + .disabled(model.configuredEngines.count <= 1) + } + } + } + + Section { + Button("Add Search Engine…") { + model.onAddSearchEngine() + } + } + } + } + + // MARK: - Types + + @MainActor + @Observable + class ViewModel + { + var configuredEngines: [ConfiguredEngine] = [] + var onAddSearchEngine: () -> Void = {} + var onChangeEngines: () -> Void = {} + + init() { + reloadConfiguredEngines() + } + + func reloadConfiguredEngines() { + let defaultEngine = Settings.shared.defaultSearchEngineName + self.configuredEngines = Settings.shared.searchEngines.map { val in + ConfiguredEngine( + name: val.key, + url: val.value, + isDefault: val.key == defaultEngine + ) + } + } + + func deleteEngine(_ id: ConfiguredEngine.ID) { + let engine = configuredEngines.first { $0.url == id }! + + var engines = Settings.shared.searchEngines + engines.removeValue(forKey: engine.name) + Settings.shared.searchEngines = engines + + if engine.isDefault { + // Pick another default + if let firstEngine = engines.first { + Settings.shared.defaultSearchEngineName = firstEngine.key + } + } + + reloadConfiguredEngines() + onChangeEngines() + } + + func makeDefault(_ id: ConfiguredEngine.ID) { + let engineName = configuredEngines.first { $0.url == id }!.name + Settings.shared.defaultSearchEngineName = engineName + + reloadConfiguredEngines() + onChangeEngines() + } + } + + struct ConfiguredEngine: Identifiable + { + let name: String + let url: String + let isDefault: Bool + + var id: String { url } + } +} + +@MainActor +class ManageSearchEnginesViewController: UIHostingController +{ + let model: ManageSearchEnginesView.ViewModel + + init() { + self.model = ManageSearchEnginesView.ViewModel() + super.init(rootView: ManageSearchEnginesView(model: model)) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#Preview { + @Previewable @State var model = ManageSearchEnginesView.ViewModel() + ManageSearchEnginesView(model: model) +} diff --git a/App/Settings/Settings.swift b/App/Settings/Settings.swift index c0983e8..76bcbd5 100644 --- a/App/Settings/Settings.swift +++ b/App/Settings/Settings.swift @@ -74,26 +74,28 @@ extension String: RawRepresentable { class Settings { static let shared = Settings() - - public enum SearchProviderSetting: String, CaseIterable { - case google = "Google" - case duckduckgo = "DuckDuckGo" - case searxnor = "Searx.nor" - case whoogle = "Whoogle.nor" - - func provider() -> SearchProvider { - switch self { - case .google: return SearchProvider.google - case .duckduckgo: return SearchProvider.duckduckgo - case .searxnor: return SearchProvider.searxnor - case .whoogle: return SearchProvider.whoogle - } + + // Map of search engine name -> URL template containing %q or %s placeholder + // Defaults preserve the previous built-in engines + @SettingProperty(key: "searchEngines") + public var searchEngines: [String: String] = [ + "Google": "https://google.com/search?q=%q&gbv=1", + "DuckDuckGo": "https://html.duckduckgo.com/html/?q=%q", + ] + + // Name of the default search engine from `searchEngines` + @SettingProperty(key: "defaultSearchEngine") + public var defaultSearchEngineName: String = "Searx.nor" + + // Convenience to build a SearchProvider from current default + func currentSearchProvider() -> SearchProvider { + if let template = searchEngines[defaultSearchEngineName] { + return SearchProvider.fromTemplate(template) } + // Fallback to Google if something goes wrong + return SearchProvider.fromTemplate("https://google.com/search?q=%q&gbv=1") } - @SettingProperty(key: "searchProvider") - public var searchProvider: SearchProviderSetting = .searxnor - @SettingProperty(key: "redirectRules") public var redirectRules: [String: String] = [:] diff --git a/App/Web Search/SearchProvider.swift b/App/Web Search/SearchProvider.swift index 1fd3c1b..da80436 100644 --- a/App/Web Search/SearchProvider.swift +++ b/App/Web Search/SearchProvider.swift @@ -9,6 +9,18 @@ import Foundation class SearchProvider { + // Build a provider from a URL template. Template should contain %q or %s + // which will be replaced with the sanitized query string. + static func fromTemplate(_ template: String) -> SearchProvider { + SearchProvider(resolver: { query in + let sanitized = query.sanitized() + let replaced = template + .replacingOccurrences(of: "%q", with: sanitized) + .replacingOccurrences(of: "%s", with: sanitized) + return URL(string: replaced) ?? URL(string: "https://google.com/search?q=\(sanitized)&gbv=1")! + }) + } + static let google = SearchProvider(resolver: { query in // gbv=1: no JS URL(string: "https://google.com/search?q=\(query.sanitized())&gbv=1")! diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index 411e95b..d8b9e88 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ CD361CF6271A3718006E9CA5 /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; }; CD470C4225DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */; }; CD470C4425DE070400AFBE0E /* BrowserViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */; }; + CD4930D92E8B390200ADDE99 /* AddSearchEngineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */; }; + CD4930DB2E8B3F6500ADDE99 /* ManageSearchEnginesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */; }; CD7313E22705349700053347 /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E12705349700053347 /* ScriptPolicyViewController.swift */; }; CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */; }; CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */; }; @@ -143,6 +145,8 @@ CD3D6CED2DA9F8910099667F /* WebKitDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebKitDefines.h; sourceTree = ""; }; CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WebKitDelegate.swift"; sourceTree = ""; }; CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Keyboard.swift"; sourceTree = ""; }; + CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSearchEngineViewController.swift; sourceTree = ""; }; + CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageSearchEnginesViewController.swift; sourceTree = ""; }; CD7313E12705349700053347 /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = ""; }; CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewControllerDelegate.swift; sourceTree = ""; }; CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -477,6 +481,8 @@ CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */, CD7A7E9E2686B29100E20BA3 /* GeneralSettingsViewController.swift */, CD7A7EA02686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift */, + CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */, + CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */, CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */, ); path = Settings; @@ -569,6 +575,7 @@ files = ( 1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */, 1AD3104325254FB900A4A952 /* FindOnPageViewController.swift in Sources */, + CD4930D92E8B390200ADDE99 /* AddSearchEngineViewController.swift in Sources */, 1A03811424E73EB300826501 /* SegmentedReliefButton.swift in Sources */, 1A03811024E71CF000826501 /* ReliefButton.swift in Sources */, CD8ACBC22DC9A2F7008BF856 /* Hacks.m in Sources */, @@ -604,6 +611,7 @@ CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, CD8DBE7B2A85D892006A0FE0 /* LayoutLatch.swift in Sources */, + CD4930DB2E8B3F6500ADDE99 /* ManageSearchEnginesViewController.swift in Sources */, CD7A7EA12686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */, @@ -784,7 +792,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = DQQH5H6GBD; + DEVELOPMENT_TEAM = 3SJALV9BQ7; INFOPLIST_FILE = "App/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Attractor; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -819,7 +827,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = DQQH5H6GBD; + DEVELOPMENT_TEAM = 3SJALV9BQ7; INFOPLIST_FILE = "App/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Attractor; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";