codex: add custom search engines
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,11 +113,16 @@ class GeneralSettingsViewController: UIViewController
|
||||
typealias Item = String
|
||||
|
||||
static let SearchProviderPopupItem = "searchProvider.popup"
|
||||
static let SearchEngineNameFieldItem = "searchEngine.add.name"
|
||||
static let SearchEngineURLFieldItem = "searchEngine.add.url"
|
||||
static let SyncServerItem = "syncServer.field"
|
||||
|
||||
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
|
||||
let collectionView: UICollectionView
|
||||
|
||||
private var pendingEngineName: String = ""
|
||||
private var pendingEngineURL: String = ""
|
||||
|
||||
static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
|
||||
@@ -180,20 +185,45 @@ class GeneralSettingsViewController: UIViewController
|
||||
|
||||
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 itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { 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()
|
||||
let menu = UIMenu(children: names.map { name in
|
||||
let action = UIAction(title: name, handler: actionHandler)
|
||||
action.state = (Settings.shared.defaultSearchEngineName == name) ? .on : .off
|
||||
return action
|
||||
})
|
||||
|
||||
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
|
||||
} else if identifier == Self.SearchEngineNameFieldItem {
|
||||
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||
text: self.pendingEngineName,
|
||||
placeholderText: "Name (e.g., Startpage)",
|
||||
textChanged: { [weak self] newString in
|
||||
self?.pendingEngineName = newString
|
||||
},
|
||||
pressedReturn: { $0.resignFirstResponder() },
|
||||
keyboardType: .default,
|
||||
returnKeyType: .next
|
||||
)
|
||||
} else if identifier == Self.SearchEngineURLFieldItem {
|
||||
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||
text: self.pendingEngineURL,
|
||||
placeholderText: "URL template (use %q or %s)",
|
||||
textChanged: { [weak self] newString in
|
||||
self?.pendingEngineURL = newString
|
||||
},
|
||||
pressedReturn: { [weak self] textField in
|
||||
guard let self = self else { return }
|
||||
self.tryAddPendingSearchEngine()
|
||||
textField.resignFirstResponder()
|
||||
},
|
||||
keyboardType: .URL,
|
||||
returnKeyType: .done
|
||||
)
|
||||
} else if identifier == Self.SyncServerItem {
|
||||
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||
text: Settings.shared.syncServer ?? "",
|
||||
@@ -248,14 +278,7 @@ class GeneralSettingsViewController: UIViewController
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -265,3 +288,33 @@ extension GeneralSettingsViewController : UICollectionViewDelegate {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private extension GeneralSettingsViewController {
|
||||
func applySnapshot(animatingDifferences: Bool) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections(Section.allCases)
|
||||
snapshot.appendItems([ Self.SearchProviderPopupItem,
|
||||
Self.SearchEngineNameFieldItem,
|
||||
Self.SearchEngineURLFieldItem ], toSection: .searchEngine)
|
||||
snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer)
|
||||
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
||||
}
|
||||
|
||||
func tryAddPendingSearchEngine() {
|
||||
let name = pendingEngineName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let url = pendingEngineURL.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
|
||||
|
||||
// Reset inputs and refresh UI
|
||||
pendingEngineName = ""
|
||||
pendingEngineURL = ""
|
||||
applySnapshot(animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,26 +74,30 @@ 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",
|
||||
"Searx.nor": "http://searx.nor/search?q=%q&categories=general",
|
||||
"Whoogle.nor": "http://whoogle.nor/search?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] = [:]
|
||||
|
||||
|
||||
@@ -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")!
|
||||
|
||||
Reference in New Issue
Block a user