codex: add custom search engines #1
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,11 +113,16 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
typealias Item = String
|
typealias Item = String
|
||||||
|
|
||||||
static let SearchProviderPopupItem = "searchProvider.popup"
|
static let SearchProviderPopupItem = "searchProvider.popup"
|
||||||
|
static let SearchEngineNameFieldItem = "searchEngine.add.name"
|
||||||
|
static let SearchEngineURLFieldItem = "searchEngine.add.url"
|
||||||
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
|
||||||
|
|
||||||
|
private var pendingEngineName: String = ""
|
||||||
|
private var pendingEngineURL: String = ""
|
||||||
|
|
||||||
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),
|
||||||
@@ -180,20 +185,45 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
|
|
||||||
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 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)
|
let menu = UIMenu(children: 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
|
||||||
})
|
})
|
||||||
|
|
||||||
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
|
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 {
|
} else if identifier == Self.SyncServerItem {
|
||||||
cell.contentConfiguration = TextFieldContentConfiguration(
|
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||||
text: Settings.shared.syncServer ?? "",
|
text: Settings.shared.syncServer ?? "",
|
||||||
@@ -248,14 +278,7 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -265,3 +288,33 @@ 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,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,24 +75,28 @@ 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",
|
||||||
|
"Searx.nor": "http://searx.nor/search?q=%q&categories=general",
|
||||||
|
"Whoogle.nor": "http://whoogle.nor/search?q=%q"
|
||||||
|
]
|
||||||
|
|
||||||
func provider() -> SearchProvider {
|
// Name of the default search engine from `searchEngines`
|
||||||
switch self {
|
@SettingProperty(key: "defaultSearchEngine")
|
||||||
case .google: return SearchProvider.google
|
public var defaultSearchEngineName: String = "Searx.nor"
|
||||||
case .duckduckgo: return SearchProvider.duckduckgo
|
|
||||||
case .searxnor: return SearchProvider.searxnor
|
|
||||||
case .whoogle: return SearchProvider.whoogle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SettingProperty(key: "searchProvider")
|
// Convenience to build a SearchProvider from current default
|
||||||
public var searchProvider: SearchProviderSetting = .searxnor
|
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: "redirectRules")
|
@SettingProperty(key: "redirectRules")
|
||||||
public var redirectRules: [String: String] = [:]
|
public var redirectRules: [String: String] = [:]
|
||||||
|
|||||||
@@ -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")!
|
||||||
|
|||||||
Reference in New Issue
Block a user