Dactyloidae/mobile/ios/Client/Frontend/Settings/CustomSearchViewController.swift
2026-06-26 21:04:09 -07:00

252 lines
9.8 KiB
Swift

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import Shared
import SnapKit
import Storage
import SDWebImage
import Deferred
private let log = Logger.browserLogger
class CustomSearchError: MaybeErrorType {
enum Reason {
case DuplicateEngine, FormInput
}
var reason: Reason!
internal var description: String {
return "Search Engine Not Added"
}
init(_ reason: Reason) {
self.reason = reason
}
}
class CustomSearchViewController: SettingsTableViewController {
fileprivate var urlString: String?
fileprivate var engineTitle = ""
fileprivate lazy var spinnerView: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.hidesWhenStopped = true
return spinner
}()
override func viewDidLoad() {
super.viewDidLoad()
title = Strings.SettingsAddCustomEngineTitle
view.addSubview(spinnerView)
spinnerView.snp.makeConstraints { make in
make.center.equalTo(self.view.snp.center)
}
}
var successCallback: (() -> Void)?
fileprivate func addSearchEngine(_ searchQuery: String, title: String) {
spinnerView.startAnimating()
let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
createEngine(forQuery: trimmedQuery, andName: trimmedTitle).uponQueue(DispatchQueue.main) { result in
self.spinnerView.stopAnimating()
guard let engine = result.successValue else {
let alert: UIAlertController
let error = result.failureValue as? CustomSearchError
alert = (error?.reason == .DuplicateEngine) ?
ThirdPartySearchAlerts.duplicateCustomEngine() : ThirdPartySearchAlerts.incorrectCustomEngineForm()
self.navigationItem.rightBarButtonItem?.isEnabled = true
self.present(alert, animated: true, completion: nil)
return
}
self.profile.searchEngines.addSearchEngine(engine)
CATransaction.begin() // Use transaction to call callback after animation has been completed
CATransaction.setCompletionBlock(self.successCallback)
_ = self.navigationController?.popViewController(animated: true)
CATransaction.commit()
}
}
func createEngine(forQuery query: String, andName name: String) -> Deferred<Maybe<OpenSearchEngine>> {
let deferred = Deferred<Maybe<OpenSearchEngine>>()
guard let template = getSearchTemplate(withString: query),
let url = URL(string: template.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed)!), url.isWebPage() else {
deferred.fill(Maybe(failure: CustomSearchError(.FormInput)))
return deferred
}
// ensure we haven't already stored this template
guard engineExists(name: name, template: template) == false else {
deferred.fill(Maybe(failure: CustomSearchError(.DuplicateEngine)))
return deferred
}
FaviconFetcher.fetchFavImageForURL(forURL: url, profile: profile).uponQueue(DispatchQueue.main) { result in
let image = result.successValue ?? FaviconFetcher.getDefaultFavicon(url)
let engine = OpenSearchEngine(engineID: nil, shortName: name, image: image, searchTemplate: template, suggestTemplate: nil, isCustomEngine: true)
//Make sure a valid scheme is used
let url = engine.searchURLForQuery("test")
let maybe = (url == nil) ? Maybe(failure: CustomSearchError(.FormInput)) : Maybe(success: engine)
deferred.fill(maybe)
}
return deferred
}
private func engineExists(name: String, template: String) -> Bool {
return profile.searchEngines.orderedEngines.contains { (engine) -> Bool in
return engine.shortName == name || engine.searchTemplate == template
}
}
func getSearchTemplate(withString query: String) -> String? {
let SearchTermComponent = "%s" //Placeholder in User Entered String
let placeholder = "{searchTerms}" //Placeholder looked for when using Custom Search Engine in OpenSearch.swift
if query.contains(SearchTermComponent) {
return query.replacingOccurrences(of: SearchTermComponent, with: placeholder)
}
return nil
}
override func generateSettings() -> [SettingSection] {
func URLFromString(_ string: String?) -> URL? {
guard let string = string else {
return nil
}
return URL(string: string)
}
let titleField = CustomSearchEngineTextView(placeholder: Strings.SettingsAddCustomEngineTitlePlaceholder, settingIsValid: { text in
return text != nil && text != ""
}, settingDidChange: {fieldText in
guard let title = fieldText else {
return
}
self.engineTitle = title
})
titleField.textField.accessibilityIdentifier = "customEngineTitle"
let urlField = CustomSearchEngineTextView(placeholder: Strings.SettingsAddCustomEngineURLPlaceholder, height: 133, settingIsValid: { text in
//Can check url text text validity here.
return true
}, settingDidChange: {fieldText in
self.urlString = fieldText
})
urlField.textField.autocapitalizationType = .none
urlField.textField.accessibilityIdentifier = "customEngineUrl"
let settings: [SettingSection] = [
SettingSection(title: NSAttributedString(string: Strings.SettingsAddCustomEngineTitleLabel), children: [titleField]),
SettingSection(title: NSAttributedString(string: Strings.SettingsAddCustomEngineURLLabel), footerTitle: NSAttributedString(string: "http://youtube.com/search?q=%s"), children: [urlField])
]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(self.addCustomSearchEngine(_:)))
self.navigationItem.rightBarButtonItem?.accessibilityIdentifier = "customEngineSaveButton"
return settings
}
func addCustomSearchEngine(_ nav: UINavigationController?) {
self.view.endEditing(true)
navigationItem.rightBarButtonItem?.isEnabled = false
if let url = self.urlString {
self.addSearchEngine(url, title: self.engineTitle)
}
}
}
class CustomSearchEngineTextView: Setting, UITextViewDelegate {
fileprivate let Padding: CGFloat = 8
fileprivate let TextLabelHeight: CGFloat = 44
fileprivate var TextFieldHeight: CGFloat = 44
fileprivate let defaultValue: String?
fileprivate let placeholder: String
fileprivate let settingDidChange: ((String?) -> Void)?
fileprivate let settingIsValid: ((String?) -> Bool)?
let textField = UITextView()
let placeholderLabel = UILabel()
init(defaultValue: String? = nil, placeholder: String, height: CGFloat = 44, settingIsValid isValueValid: ((String?) -> Bool)? = nil, settingDidChange: ((String?) -> Void)? = nil) {
self.defaultValue = defaultValue
self.TextFieldHeight = height
self.settingDidChange = settingDidChange
self.settingIsValid = isValueValid
self.placeholder = placeholder
textField.addSubview(placeholderLabel)
super.init(cellHeight: TextFieldHeight)
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
if let id = accessibilityIdentifier {
textField.accessibilityIdentifier = id + "TextField"
}
placeholderLabel.adjustsFontSizeToFitWidth = true
placeholderLabel.textColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
placeholderLabel.text = placeholder
placeholderLabel.frame = CGRect(x: 0, y: 0, width: textField.frame.width, height: TextLabelHeight)
textField.font = placeholderLabel.font
textField.textContainer.lineFragmentPadding = 0
textField.keyboardType = .URL
textField.autocorrectionType = .no
textField.delegate = self
cell.isUserInteractionEnabled = true
cell.accessibilityTraits = UIAccessibilityTraitNone
cell.contentView.addSubview(textField)
cell.selectionStyle = .none
textField.snp.makeConstraints { make in
make.height.equalTo(TextFieldHeight)
make.left.right.equalTo(cell.contentView).inset(Padding)
}
}
override func onClick(_ navigationController: UINavigationController?) {
textField.becomeFirstResponder()
}
fileprivate func isValid(_ value: String?) -> Bool {
guard let test = settingIsValid else {
return true
}
return test(prepareValidValue(userInput: value))
}
func prepareValidValue(userInput value: String?) -> String? {
return value
}
func textViewDidBeginEditing(_ textView: UITextView) {
placeholderLabel.isHidden = textField.text != ""
}
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = textField.text != ""
settingDidChange?(textView.text)
let color = isValid(textField.text) ? UIConstants.TableViewRowTextColor : UIConstants.DestructiveRed
textField.textColor = color
}
func textViewDidEndEditing(_ textView: UITextView) {
placeholderLabel.isHidden = textField.text != ""
settingDidChange?(textView.text)
}
}