Files
Attractor/App/Settings/GeneralSettingsViewController.swift

321 lines
13 KiB
Swift
Raw Normal View History

2021-06-29 18:09:42 -07:00
//
// GeneralSettingsViewController.swift
// GeneralSettingsViewController
//
// Created by James Magahern on 6/25/21.
//
import UIKit
struct LabelContentConfiguration : UIContentConfiguration
{
var text: String = ""
var insets: UIEdgeInsets = .zero
var textAlignment: NSTextAlignment = .natural
func makeContentView() -> UIView & UIContentView {
let label = UILabel(frame: .zero)
let contentView = GenericContentView<UILabel, LabelContentConfiguration>(configuration: self, view: label) { config, label in
label.text = config.text
label.textAlignment = config.textAlignment
}
contentView.insets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
return contentView
}
func updated(for state: UIConfigurationState) -> Self {
self
}
}
2022-08-05 18:55:19 -07:00
struct TextFieldContentConfiguration : UIContentConfiguration
{
var text: String = ""
var placeholderText: String? = nil
2023-04-19 14:30:06 -07:00
var textChanged: ((String) -> Void)? = nil
var pressedReturn: ((UITextField) -> Void)? = nil
var keyboardType: UIKeyboardType = .default
var returnKeyType: UIReturnKeyType = .default
2022-08-05 18:55:19 -07:00
func makeContentView() -> UIView & UIContentView {
let textField = UITextField(frame: .zero)
textField.borderStyle = .roundedRect
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
2023-04-19 14:30:06 -07:00
textField.keyboardType = keyboardType
textField.returnKeyType = returnKeyType
2022-08-05 18:55:19 -07:00
return GenericContentView<UITextField, TextFieldContentConfiguration>(configuration: self, view: textField) { config, textField in
textField.text = config.text
textField.placeholder = config.placeholderText
2023-04-19 14:30:06 -07:00
if let textChanged = config.textChanged {
textField.addAction(UIAction { _ in textChanged(textField.text ?? "") }, for: .editingChanged)
}
if let pressedReturn = config.pressedReturn {
class ReturnDelegate : NSObject, UITextFieldDelegate {
var pressedReturn: ((UITextField) -> Void)
public init(pressedReturn: @escaping ((UITextField) -> Void)) {
self.pressedReturn = pressedReturn
}
2023-04-19 14:33:17 -07:00
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
2023-04-19 14:30:06 -07:00
pressedReturn(textField)
return false
}
}
let delegate = ReturnDelegate(pressedReturn: pressedReturn)
objc_setAssociatedObject(textField, "returnDelegate", delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
textField.delegate = delegate
}
2022-08-05 18:55:19 -07:00
}
}
func updated(for state: UIConfigurationState) -> TextFieldContentConfiguration {
self
}
}
2021-06-29 18:09:42 -07:00
struct ButtonContentConfiguration : UIContentConfiguration
{
var menu: UIMenu
func makeContentView() -> UIView & UIContentView {
let button = UIButton(primaryAction: nil)
return GenericContentView<UIButton, ButtonContentConfiguration>(configuration: self, view: button) { config, button in
button.menu = menu
button.showsMenuAsPrimaryAction = true
if #available(macCatalyst 15.0, iOS 15.0, *) {
var buttonConfiguration = UIButton.Configuration.plain()
buttonConfiguration.titleAlignment = .trailing
button.configuration = buttonConfiguration
button.changesSelectionAsPrimaryAction = true
}
}
}
func updated(for state: UIConfigurationState) -> ButtonContentConfiguration {
self
}
}
class GeneralSettingsViewController: UIViewController
{
enum Section: String, CaseIterable {
case searchEngine = "Search Engine"
2022-08-05 18:55:19 -07:00
case syncServer = "Sync Server"
2021-06-29 18:09:42 -07:00
}
typealias Item = String
static let SearchProviderPopupItem = "searchProvider.popup"
2025-09-28 21:10:31 -07:00
static let SearchEngineNameFieldItem = "searchEngine.add.name"
static let SearchEngineURLFieldItem = "searchEngine.add.url"
2022-08-05 18:55:19 -07:00
static let SyncServerItem = "syncServer.field"
2021-06-29 18:09:42 -07:00
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
let collectionView: UICollectionView
2025-09-28 21:10:31 -07:00
private var pendingEngineName: String = ""
private var pendingEngineURL: String = ""
2021-06-29 18:09:42 -07:00
static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout {
2021-11-29 10:56:14 -10:00
#if targetEnvironment(macCatalyst)
2021-06-29 18:09:42 -07:00
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)
2021-06-29 18:09:42 -07:00
)
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
2021-11-29 10:56:14 -10:00
#else
2021-06-29 18:09:42 -07:00
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
listConfiguration.headerMode = .supplementary
return UICollectionViewCompositionalLayout.list(using: listConfiguration)
2021-11-29 10:56:14 -10:00
#endif
2021-06-29 18:09:42 -07:00
}
static func sectionHeaderConfiguration(forIdiom idiom: UIUserInterfaceIdiom, sectionName: String) -> UIContentConfiguration {
if idiom == .mac {
return LabelContentConfiguration(
text: sectionName + ": ",
2022-08-05 18:55:19 -07:00
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0, right: 10.0),
2021-06-29 18:09:42 -07:00
textAlignment: .right
)
} else {
var config = UIListContentConfiguration.plainHeader()
config.text = sectionName
return config
}
}
#if targetEnvironment(macCatalyst)
static let staticIdiom = UIUserInterfaceIdiom.mac
#else
static let staticIdiom = UIUserInterfaceIdiom.pad
#endif
init() {
let actionHandler = { (action: UIAction) in
2025-09-28 21:10:31 -07:00
let engineName = action.title
Settings.shared.defaultSearchEngineName = engineName
2021-06-29 18:09:42 -07:00
}
let itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, identifier in
if identifier == Self.SearchProviderPopupItem {
2025-09-28 21:10:31 -07:00
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
2021-06-29 18:09:42 -07:00
return action
})
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
2025-09-28 21:10:31 -07:00
} 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
)
2022-08-05 18:55:19 -07:00
} else if identifier == Self.SyncServerItem {
cell.contentConfiguration = TextFieldContentConfiguration(
text: Settings.shared.syncServer ?? "",
2022-08-05 18:55:19 -07:00
placeholderText: "https://sync.server.com",
textChanged: { newString in
Settings.shared.syncServer = newString
2023-04-19 14:30:06 -07:00
},
pressedReturn: { $0.resignFirstResponder() },
keyboardType: .URL,
returnKeyType: .done
2022-08-05 18:55:19 -07:00
)
2021-06-29 18:09:42 -07:00
}
2022-08-05 18:55:19 -07:00
#if !targetEnvironment(macCatalyst)
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
#endif
2021-06-29 18:09:42 -07:00
}
let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: {
(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<Section, Item>(collectionView: collectionView) {
(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)
}
super.init(nibName: nil, bundle: nil)
2023-04-19 14:30:06 -07:00
collectionView.delegate = self
2021-06-29 18:09:42 -07:00
tabBarItem.title = "General"
tabBarItem.image = UIImage(systemName: "gear")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = collectionView
}
override func viewDidLoad() {
super.viewDidLoad()
2025-09-28 21:10:31 -07:00
applySnapshot(animatingDifferences: false)
2021-06-29 18:09:42 -07:00
}
}
2023-04-19 14:30:06 -07:00
extension GeneralSettingsViewController : UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
false
}
}
2025-09-28 21:10:31 -07:00
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)
}
}