3 Commits

Author SHA1 Message Date
e53cde7f60 Merge pull request 'codex: add custom search engines' (#1) from feature/custom-search-engines into master
Reviewed-on: #1

Good on iOS, still need to do it on macOS (#2)
2025-09-29 23:09:41 +00:00
62e36359b8 finish implementation for iOS 2025-09-29 16:09:00 -07:00
265d393cdc codex: add custom search engines 2025-09-28 21:10:31 -07:00
8 changed files with 433 additions and 100 deletions

View File

@@ -795,7 +795,7 @@ extension BrowserViewController: UITextFieldDelegate
currentTab.beginLoadingURL(url) currentTab.beginLoadingURL(url)
} else { } else {
let searchURL = Settings.shared.searchProvider.provider().searchURLWithQuery(text) let searchURL = Settings.shared.currentSearchProvider().searchURLWithQuery(text)
currentTab.beginLoadingURL(searchURL) currentTab.beginLoadingURL(searchURL)
} }

View File

@@ -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<AddSearchEngineView>
{
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)
}

View File

@@ -23,8 +23,8 @@ struct AmberSettingsView: View {
@Environment(\.presentationMode) @Environment(\.presentationMode)
@Binding private var presentationMode @Binding private var presentationMode
@State private var searchProvider = Settings.shared.searchProvider { @State private var defaultSearchEngineName = Settings.shared.defaultSearchEngineName {
didSet { Settings.shared.searchProvider = searchProvider } didSet { Settings.shared.defaultSearchEngineName = defaultSearchEngineName }
} }
var body: some View { var body: some View {
@@ -35,12 +35,12 @@ struct AmberSettingsView: View {
}) })
Section(header: Text("Search Provider"), content: { Section(header: Text("Search Provider"), content: {
ForEach(Settings.SearchProviderSetting.allCases, id: \.self, content: { setting in ForEach(Array(Settings.shared.searchEngines.keys).sorted(), id: \.self, content: { name in
Button(action: { searchProvider = setting }, label: { Button(action: { defaultSearchEngineName = name }, label: {
HStack { HStack {
Text(setting.rawValue) Text(name)
Spacer() Spacer()
if searchProvider == setting { if defaultSearchEngineName == name {
Image(systemName: "checkmark") Image(systemName: "checkmark")
} }
} }

View File

@@ -109,55 +109,56 @@ class GeneralSettingsViewController: UIViewController
case searchEngine = "Search Engine" case searchEngine = "Search Engine"
case syncServer = "Sync Server" case syncServer = "Sync Server"
} }
typealias Item = String typealias Item = String
static let SearchProviderPopupItem = "searchProvider.popup" static let SearchProviderPopupItem = "searchProvider.popup"
static let SyncServerItem = "syncServer.field" static let SyncServerItem = "syncServer.field"
let dataSource: UICollectionViewDiffableDataSource<Section, Item> let dataSource: UICollectionViewDiffableDataSource<Section, Item>
let collectionView: UICollectionView let collectionView: UICollectionView
let viewModel: ViewModel
static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout { static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)) heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize) let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.60), let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.60),
heightDimension: .absolute(44)) 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 section = NSCollectionLayoutSection(group: group) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
// section.interGroupSpacing = spacing group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(1.0), top: nil, trailing: nil, bottom: nil)
section.contentInsets = insets
section.supplementariesFollowContentInsets = true
section.boundarySupplementaryItems = [ sectionHeader ]
let layout = UICollectionViewCompositionalLayout(section: section) let insets = NSDirectionalEdgeInsets(top: 24.0, leading: 64.0, bottom: 24.0, trailing: 64.0)
return layout
#else let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.40),
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) heightDimension: .estimated(44.0))
listConfiguration.headerMode = .supplementary let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
return UICollectionViewCompositionalLayout.list(using: listConfiguration) layoutSize: headerFooterSize,
#endif 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 { static func sectionHeaderConfiguration(forIdiom idiom: UIUserInterfaceIdiom, sectionName: String) -> UIContentConfiguration {
if idiom == .mac { if idiom == .mac {
return LabelContentConfiguration( return LabelContentConfiguration(
@@ -166,33 +167,41 @@ class GeneralSettingsViewController: UIViewController
textAlignment: .right textAlignment: .right
) )
} else { } else {
var config = UIListContentConfiguration.plainHeader() var config = UIListContentConfiguration.header()
config.text = sectionName config.text = sectionName
return config return config
} }
} }
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
static let staticIdiom = UIUserInterfaceIdiom.mac static let staticIdiom = UIUserInterfaceIdiom.mac
#else #else
static let staticIdiom = UIUserInterfaceIdiom.pad static let staticIdiom = UIUserInterfaceIdiom.pad
#endif #endif
init() { init() {
let actionHandler = { (action: UIAction) in let actionHandler = { (action: UIAction) in
let providerString = action.title let engineName = action.title
let provider = Settings.SearchProviderSetting(rawValue: providerString)! Settings.shared.defaultSearchEngineName = engineName
Settings.shared.searchProvider = provider
} }
let viewModel = ViewModel()
let itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, identifier in let itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, identifier in
if identifier == Self.SearchProviderPopupItem { if identifier == Self.SearchProviderPopupItem {
let menu = UIMenu(children: Settings.SearchProviderSetting.allCases.map { provider in let names = Settings.shared.searchEngines.keys.sorted()
let action = UIAction(title: provider.rawValue, handler: actionHandler) var engineMenuItems: [UIMenuElement] = names.map { name in
action.state = Settings.shared.searchProvider == provider ? .on : .off let action = UIAction(title: name, handler: actionHandler)
action.state = (Settings.shared.defaultSearchEngineName == name) ? .on : .off
return action 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) cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
} else if identifier == Self.SyncServerItem { } else if identifier == Self.SyncServerItem {
cell.contentConfiguration = TextFieldContentConfiguration( cell.contentConfiguration = TextFieldContentConfiguration(
@@ -206,58 +215,114 @@ class GeneralSettingsViewController: UIViewController
returnKeyType: .done returnKeyType: .done
) )
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() cell.backgroundConfiguration = UIBackgroundConfiguration.listCell()
#endif #endif
} }
let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: { let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: {
(cell, string, indexPath) in (cell, string, indexPath) in
let sectionName = Section.allCases[indexPath.section].rawValue let sectionName = Section.allCases[indexPath.section].rawValue
cell.contentConfiguration = Self.sectionHeaderConfiguration(forIdiom: Self.staticIdiom, sectionName: sectionName) cell.contentConfiguration = Self.sectionHeaderConfiguration(forIdiom: Self.staticIdiom, sectionName: sectionName)
}) })
let layout = Self.createLayout(forIdiom: Self.staticIdiom) let layout = Self.createLayout(forIdiom: Self.staticIdiom)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .systemGroupedBackground collectionView.backgroundColor = .systemGroupedBackground
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
(collectionView, indexPath, identifier) in (collectionView, indexPath, identifier) in
return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistry, for: indexPath, item: identifier) return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistry, for: indexPath, item: identifier)
} }
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistry, for: indexPath) return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistry, for: indexPath)
} }
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
collectionView.delegate = self collectionView.delegate = self
tabBarItem.title = "General" tabBarItem.title = "General"
tabBarItem.image = UIImage(systemName: "gear") 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func loadView() {
self.view = collectionView self.view = collectionView
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
applySnapshot(animatingDifferences: false)
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)
} }
// MARK: - Types
@MainActor
@Observable
class ViewModel
{
var onManageSearchEngines: () -> Void = {}
}
} }
extension GeneralSettingsViewController : UICollectionViewDelegate { extension GeneralSettingsViewController : UICollectionViewDelegate {
@@ -265,3 +330,36 @@ extension GeneralSettingsViewController : UICollectionViewDelegate {
false false
} }
} }
private extension GeneralSettingsViewController {
func applySnapshot(animatingDifferences: Bool) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
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)
}
}

View File

@@ -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<ManageSearchEnginesView>
{
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)
}

View File

@@ -74,26 +74,28 @@ extension String: RawRepresentable {
class Settings class Settings
{ {
static let shared = Settings() static let shared = Settings()
public enum SearchProviderSetting: String, CaseIterable { // Map of search engine name -> URL template containing %q or %s placeholder
case google = "Google" // Defaults preserve the previous built-in engines
case duckduckgo = "DuckDuckGo" @SettingProperty(key: "searchEngines")
case searxnor = "Searx.nor" public var searchEngines: [String: String] = [
case whoogle = "Whoogle.nor" "Google": "https://google.com/search?q=%q&gbv=1",
"DuckDuckGo": "https://html.duckduckgo.com/html/?q=%q",
func provider() -> SearchProvider { ]
switch self {
case .google: return SearchProvider.google // Name of the default search engine from `searchEngines`
case .duckduckgo: return SearchProvider.duckduckgo @SettingProperty(key: "defaultSearchEngine")
case .searxnor: return SearchProvider.searxnor public var defaultSearchEngineName: String = "Searx.nor"
case .whoogle: return SearchProvider.whoogle
} // 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") @SettingProperty(key: "redirectRules")
public var redirectRules: [String: String] = [:] public var redirectRules: [String: String] = [:]

View File

@@ -9,6 +9,18 @@ import Foundation
class SearchProvider 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 static let google = SearchProvider(resolver: { query in
// gbv=1: no JS // gbv=1: no JS
URL(string: "https://google.com/search?q=\(query.sanitized())&gbv=1")! URL(string: "https://google.com/search?q=\(query.sanitized())&gbv=1")!

View File

@@ -44,6 +44,8 @@
CD361CF6271A3718006E9CA5 /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; }; CD361CF6271A3718006E9CA5 /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; };
CD470C4225DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */; }; 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 */; }; 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 */; }; CD7313E22705349700053347 /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E12705349700053347 /* ScriptPolicyViewController.swift */; };
CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */; }; CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */; };
CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.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 = "<group>"; }; CD3D6CED2DA9F8910099667F /* WebKitDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebKitDefines.h; sourceTree = "<group>"; };
CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WebKitDelegate.swift"; sourceTree = "<group>"; }; CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WebKitDelegate.swift"; sourceTree = "<group>"; };
CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Keyboard.swift"; sourceTree = "<group>"; }; CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Keyboard.swift"; sourceTree = "<group>"; };
CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSearchEngineViewController.swift; sourceTree = "<group>"; };
CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageSearchEnginesViewController.swift; sourceTree = "<group>"; };
CD7313E12705349700053347 /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = "<group>"; }; CD7313E12705349700053347 /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = "<group>"; };
CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewControllerDelegate.swift; sourceTree = "<group>"; }; CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewControllerDelegate.swift; sourceTree = "<group>"; };
CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; }; CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -477,6 +481,8 @@
CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */, CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */,
CD7A7E9E2686B29100E20BA3 /* GeneralSettingsViewController.swift */, CD7A7E9E2686B29100E20BA3 /* GeneralSettingsViewController.swift */,
CD7A7EA02686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift */, CD7A7EA02686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift */,
CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */,
CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */,
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */, CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */,
); );
path = Settings; path = Settings;
@@ -569,6 +575,7 @@
files = ( files = (
1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */, 1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */,
1AD3104325254FB900A4A952 /* FindOnPageViewController.swift in Sources */, 1AD3104325254FB900A4A952 /* FindOnPageViewController.swift in Sources */,
CD4930D92E8B390200ADDE99 /* AddSearchEngineViewController.swift in Sources */,
1A03811424E73EB300826501 /* SegmentedReliefButton.swift in Sources */, 1A03811424E73EB300826501 /* SegmentedReliefButton.swift in Sources */,
1A03811024E71CF000826501 /* ReliefButton.swift in Sources */, 1A03811024E71CF000826501 /* ReliefButton.swift in Sources */,
CD8ACBC22DC9A2F7008BF856 /* Hacks.m in Sources */, CD8ACBC22DC9A2F7008BF856 /* Hacks.m in Sources */,
@@ -604,6 +611,7 @@
CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */, CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */,
1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */,
CD8DBE7B2A85D892006A0FE0 /* LayoutLatch.swift in Sources */, CD8DBE7B2A85D892006A0FE0 /* LayoutLatch.swift in Sources */,
CD4930DB2E8B3F6500ADDE99 /* ManageSearchEnginesViewController.swift in Sources */,
CD7A7EA12686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift in Sources */, CD7A7EA12686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift in Sources */,
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */,
CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */, CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */,
@@ -784,7 +792,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 5;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DQQH5H6GBD; DEVELOPMENT_TEAM = 3SJALV9BQ7;
INFOPLIST_FILE = "App/Supporting Files/Info.plist"; INFOPLIST_FILE = "App/Supporting Files/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Attractor; INFOPLIST_KEY_CFBundleDisplayName = Attractor;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
@@ -819,7 +827,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 5;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DQQH5H6GBD; DEVELOPMENT_TEAM = 3SJALV9BQ7;
INFOPLIST_FILE = "App/Supporting Files/Info.plist"; INFOPLIST_FILE = "App/Supporting Files/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Attractor; INFOPLIST_KEY_CFBundleDisplayName = Attractor;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";