iOS/Sources/App/Settings/SettingsDetailViewControlle...

1040 lines
42 KiB
Swift

import CoreMotion
import Eureka
import FirebaseMessaging
import Intents
import IntentsUI
import PromiseKit
import RealmSwift
import Shared
import UIKit
import Version
enum SettingsDetailsGroup: String {
case display
case actions
case general
case location
case privacy
case carPlay
}
class SettingsDetailViewController: HAFormViewController, TypedRowControllerType {
var row: RowOf<ButtonRow>!
/// A closure to be called when the controller disappears.
public var onDismissCallback: ((UIViewController) -> Void)?
var detailGroup: SettingsDetailsGroup = .display
var doneButton: Bool = false
private let realm = Current.realm()
private var notificationTokens: [NotificationToken] = []
private var notificationCenterTokens: [AnyObject] = []
private var reorderingRows: [String: BaseRow] = [:]
deinit {
notificationCenterTokens.forEach(NotificationCenter.default.removeObserver)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if doneButton {
navigationItem.rightBarButtonItem = nil
doneButton = false
}
onDismissCallback?(self)
}
// swiftlint:disable:next cyclomatic_complexity
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if doneButton {
let closeSelector = #selector(SettingsDetailViewController.closeSettingsDetailView(_:))
let doneButton = UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: closeSelector
)
navigationItem.setRightBarButton(doneButton, animated: true)
}
switch detailGroup {
case .general:
title = L10n.SettingsDetails.General.title
form
+++ Section {
$0.hidden = .isCatalyst
}
<<< PushRow<AppIcon>("appIcon") {
$0.hidden = .isCatalyst
$0.title = L10n.SettingsDetails.General.AppIcon.title
$0.selectorTitle = $0.title
$0.options = AppIcon.allCases.sorted { a, b in
switch (a.isDefault, b.isDefault) {
case (true, false): return true
case (false, true): return false
default:
// swift sort isn't stable
return AppIcon.allCases.firstIndex(of: a)! < AppIcon.allCases.firstIndex(of: b)!
}
}
$0.value = AppIcon.Release
if let altIconName = UIApplication.shared.alternateIconName,
let icon = AppIcon(rawValue: altIconName) {
$0.value = icon
}
$0.displayValueFor = { $0?.title }
}.onPresent { [weak self] _, to in
to.selectableRowCellUpdate = { cell, row in
cell.height = { 72 }
cell.imageView?.layer.masksToBounds = true
cell.imageView?.layer.cornerRadius = 12.63
guard let newIcon = row.selectableValue else { return }
cell.imageView?.image = self?.resizeImage(
image: UIImage(named: newIcon.rawValue),
newSize: .init(width: 64, height: 64)
)
cell.textLabel?.text = newIcon.title
}
}.onChange { row in
let iconName = row.value?.iconName
UIApplication.shared.setAlternateIconName(iconName) { error in
Current.Log
.info("set icon to \(String(describing: iconName)) error: \(String(describing: error))")
}
}
+++ Section {
$0.hidden = .isNotCatalyst
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.General.LaunchOnLogin.title
#if targetEnvironment(macCatalyst)
let launcherIdentifier = AppConstants.BundleID.appending(".Launcher")
$0.value = Current.macBridge.isLoginItemEnabled(forBundleIdentifier: launcherIdentifier)
$0.onChange { row in
let success = Current.macBridge.setLoginItem(
forBundleIdentifier: launcherIdentifier,
enabled: row.value ?? false
)
if !success {
row.value = Current.macBridge.isLoginItemEnabled(forBundleIdentifier: launcherIdentifier)
row.updateCell()
}
}
#endif
}
<<< PushRow<SettingsStore.LocationVisibility> {
$0.tag = "locationVisibility"
$0.title = L10n.SettingsDetails.General.Visibility.title
$0.options = SettingsStore.LocationVisibility.allCases
$0.value = Current.settingsStore.locationVisibility
$0.displayValueFor = {
switch $0 ?? .dock {
case .dock: return L10n.SettingsDetails.General.Visibility.Options.dock
case .dockAndMenuBar: return L10n.SettingsDetails.General.Visibility.Options.dockAndMenuBar
case .menuBar: return L10n.SettingsDetails.General.Visibility.Options.menuBar
}
}
$0.onChange { row in
Current.settingsStore.locationVisibility = row.value ?? .dock
}
}
<<< ButtonRow { row in
row.title = L10n.SettingsDetails.General.MenuBarText.title
row.cellStyle = .value1
row.value = Current.settingsStore.menuItemTemplate?.template
row.displayValueFor = { $0 }
row.hidden = .function(["locationVisibility"], { form in
if let row = form
.rowBy(tag: "locationVisibility") as? PushRow<SettingsStore.LocationVisibility> {
return row.value?.isStatusItemVisible == false
} else {
return true
}
})
row.presentationMode = .show(controllerProvider: .callback(builder: {
if let current = Current.settingsStore.menuItemTemplate {
return TemplateEditViewController(
server: current.server,
initial: current.template,
saveHandler: { Current.settingsStore.menuItemTemplate = ($0, $1) }
)
} else {
return UIViewController()
}
}), onDismiss: { [weak self, row] _ in
row.value = Current.settingsStore.menuItemTemplate?.template
self?.navigationController?.popViewController(animated: true)
})
}
+++ Section {
$0.hidden = .function([], { _ in !Current.updater.isSupported })
}
<<< SwitchRow("checkForUpdates") {
$0.title = L10n.SettingsDetails.Updates.CheckForUpdates.title
$0.value = Current.settingsStore.privacy.updates
$0.onChange { row in
Current.settingsStore.privacy.updates = row.value ?? true
}
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Updates.CheckForUpdates.includeBetas
$0.value = Current.settingsStore.privacy.updatesIncludeBetas
$0.onChange { row in
Current.settingsStore.privacy.updatesIncludeBetas = row.value ?? true
}
}
+++ PushRow<OpenInBrowser>("openInBrowser") {
$0.hidden = .isCatalyst
$0.title = L10n.SettingsDetails.General.OpenInBrowser.title
if let value = prefs.string(forKey: "openInBrowser").flatMap({ OpenInBrowser(rawValue: $0) }),
value.isInstalled {
$0.value = value
} else {
$0.value = .Safari
}
$0.selectorTitle = $0.title
$0.options = OpenInBrowser.allCases.filter(\.isInstalled)
$0.displayValueFor = { $0?.title }
}.onChange { row in
guard let browserChoice = row.value else { return }
prefs.setValue(browserChoice.rawValue, forKey: "openInBrowser")
}
<<< SwitchRow("openInPrivateTab") {
$0.hidden = .function(["openInBrowser"], { form in
if let row = form
.rowBy(tag: "openInBrowser") as? PushRow<OpenInBrowser> {
return row.value?.supportsPrivateTabs == false
} else {
return true
}
})
$0.title = L10n.SettingsDetails.General.OpenInPrivateTab.title
$0.value = prefs.bool(forKey: "openInPrivateTab")
}.onChange { row in
prefs.setValue(row.value, forKey: "openInPrivateTab")
}
<<< SwitchRow("confirmBeforeOpeningUrl") {
$0.title = L10n.SettingsDetails.Notifications.PromptToOpenUrls.title
$0.value = prefs.bool(forKey: "confirmBeforeOpeningUrl")
}.onChange { row in
prefs.setValue(row.value, forKey: "confirmBeforeOpeningUrl")
}
+++ SwitchRow {
// mac has a system-level setting for state restoration
$0.hidden = .isCatalyst
$0.title = L10n.SettingsDetails.General.Restoration.title
$0.value = Current.settingsStore.restoreLastURL
$0.onChange { row in
Current.settingsStore.restoreLastURL = row.value ?? false
}
}
<<< PushRow<SettingsStore.PageZoom> { row in
row.title = L10n.SettingsDetails.General.PageZoom.title
row.options = SettingsStore.PageZoom.allCases
row.value = Current.settingsStore.pageZoom
row.onChange { row in
Current.settingsStore.pageZoom = row.value ?? .default
}
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.General.PinchToZoom.title
$0.hidden = .isCatalyst
$0.value = Current.settingsStore.pinchToZoom
$0.onChange { row in
Current.settingsStore.pinchToZoom = row.value ?? false
}
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.General.FullScreen.title
$0.hidden = .isCatalyst
$0.value = Current.settingsStore.fullScreen
$0.onChange { row in
Current.settingsStore.fullScreen = row.value ?? false
}
}
case .location:
title = L10n.SettingsDetails.Location.title
form
+++ locationPermissionsSection()
+++ ButtonRow {
$0.title = L10n.Settings.LocationHistory.title
$0.presentationMode = .show(controllerProvider: .callback(builder: {
LocationHistoryListViewController()
}), onDismiss: nil)
}
<<< ButtonRowWithLoading {
$0.title = L10n.SettingsDetails.Location.updateLocation
$0.onCellSelection { [weak self] _, row in
row.value = true
row.updateCell()
firstly {
HomeAssistantAPI.manuallyUpdate(
applicationState: UIApplication.shared.applicationState,
type: .userRequested
)
}.ensure {
row.value = false
row.updateCell()
}.catch { error in
let alert = UIAlertController(
title: nil,
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .cancel, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
}
+++ Section(
header: L10n.SettingsDetails.Location.Updates.header,
footer: L10n.SettingsDetails.Location.Updates.footer
)
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Location.Updates.Zone.title
$0.value = Current.settingsStore.locationSources.zone
$0.disabled = .location(conditions: [.permissionNotAlways, .accuracyNotFull])
}.onChange({ row in
Current.settingsStore.locationSources.zone = row.value ?? true
})
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Location.Updates.Background.title
$0.value = Current.settingsStore.locationSources.backgroundFetch
$0.disabled = .location(conditions: [
.permissionNotAlways,
.backgroundRefreshNotAvailable,
])
$0.hidden = .isCatalyst
}.onChange({ row in
Current.settingsStore.locationSources.backgroundFetch = row.value ?? true
})
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Location.Updates.Significant.title
$0.value = Current.settingsStore.locationSources.significantLocationChange
$0.disabled = .location(conditions: [.permissionNotAlways])
}.onChange({ row in
Current.settingsStore.locationSources.significantLocationChange = row.value ?? true
})
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Location.Updates.Notification.title
$0.value = Current.settingsStore.locationSources.pushNotifications
$0.disabled = .location(conditions: [.permissionNotAlways])
}.onChange({ row in
Current.settingsStore.locationSources.pushNotifications = row.value ?? true
})
let zoneEntities = realm.objects(RLMZone.self)
for zone in zoneEntities {
form
+++ Section(header: zone.Name, footer: "") {
$0.tag = zone.identifier
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Location.Zones.EnterExitTracked.title
$0.value = zone.TrackingEnabled
$0.disabled = Condition(booleanLiteral: true)
}
<<< LocationRow {
$0.title = L10n.SettingsDetails.Location.Zones.Location.title
$0.value = zone.location
}
<<< LabelRow {
$0.title = L10n.SettingsDetails.Location.Zones.Radius.title
$0.value = L10n.SettingsDetails.Location.Zones.Radius.label(Int(zone.Radius))
}
<<< LabelRow {
$0.title = L10n.SettingsDetails.Location.Zones.BeaconUuid.title
$0.value = zone.BeaconUUID
$0.hidden = Condition(booleanLiteral: zone.BeaconUUID == nil)
}
<<< LabelRow {
$0.title = L10n.SettingsDetails.Location.Zones.BeaconMajor.title
if let major = zone.BeaconMajor.value {
$0.value = String(describing: major)
} else {
$0.value = L10n.SettingsDetails.Location.Zones.Beacon.PropNotSet.value
}
$0.hidden = Condition(booleanLiteral: zone.BeaconMajor.value == nil)
}
<<< LabelRow {
$0.title = L10n.SettingsDetails.Location.Zones.BeaconMinor.title
if let minor = zone.BeaconMinor.value {
$0.value = String(describing: minor)
} else {
$0.value = L10n.SettingsDetails.Location.Zones.Beacon.PropNotSet.value
}
$0.hidden = Condition(booleanLiteral: zone.BeaconMinor.value == nil)
}
}
if zoneEntities.count > 0 {
form +++ InfoLabelRow {
$0.title = L10n.SettingsDetails.Location.Zones.footer
}
}
case .actions:
title = L10n.SettingsDetails.Actions.title
let actions = realm.objects(Action.self)
.sorted(byKeyPath: "Position")
.filter("Scene == nil")
let actionsFooter = Current.isCatalyst ?
L10n.SettingsDetails.Actions.footerMac : L10n.SettingsDetails.Actions.footer
let learnAboutActionsButton = SettingsButtonRow {
$0.title = L10n.SettingsDetails.Actions.Learn.Button.title
$0.accessoryIcon = .openInNewIcon
$0.onCellSelection { _, row in
guard let url = URL(string: "https://companion.home-assistant.io/docs/core/actions/") else { return }
UIApplication.shared.open(url)
row.deselect(animated: true)
}
}
form +++ learnAboutActionsButton
form +++ MultivaluedSection(
multivaluedOptions: [.Insert, .Delete, .Reorder],
header: "",
footer: actionsFooter
) { section in
section.tag = "actions"
section.multivaluedRowToInsertAt = { [weak self] _ -> ButtonRowWithPresent<ActionConfigurator> in
self?.getActionRow(nil) ?? .init()
}
section.addButtonProvider = { _ in
ButtonRow {
$0.title = L10n.addButtonLabel
$0.cellStyle = .value1
$0.tag = "add_action"
}.cellUpdate { cell, _ in
cell.textLabel?.textAlignment = .left
}
}
for action in actions.filter("isServerControlled == false") {
section <<< getActionRow(action)
}
}
form +++ RealmSection(
header: L10n.SettingsDetails.Actions.ActionsSynced.header,
footer: nil,
collection: AnyRealmCollection(actions.filter("isServerControlled == true")),
emptyRows: [
LabelRow {
$0.title = L10n.SettingsDetails.Actions.ActionsSynced.empty
$0.disabled = true
},
], getter: { [weak self] in self?.getActionRow($0) },
didUpdate: { section, collection in
if collection.isEmpty {
section.footer = HeaderFooterView(
title: L10n.SettingsDetails.Actions.ActionsSynced.footerNoActions
)
} else {
section.footer = HeaderFooterView(
title: L10n.SettingsDetails.Actions.ActionsSynced.footer
)
}
}
)
form +++ ButtonRow {
$0.title = L10n.SettingsDetails.Actions.ServerControlled.Update.title
$0.onCellSelection { _, _ in
let result = Current.modelManager.fetch()
result.pipe { result in
switch result {
case .fulfilled:
break
case let .rejected(error):
Current.Log.error("Failed to manually update server Actions: \(error.localizedDescription)")
}
}
}
}
let scenes = realm.objects(RLMScene.self).sorted(byKeyPath: RLMScene.positionKeyPath)
let toggleAllSwitch = SwitchRow()
with(toggleAllSwitch) {
$0.title = L10n.SettingsDetails.Actions.Scenes.selectAll
$0.onChange { [weak self] row in
guard let self,
let value = row.value else { return }
realm.reentrantWrite {
for scene in scenes {
scene.actionEnabled = value
}
}
}
}
var scenesUpdateObserver: NotificationToken?
toggleAllSwitch.cellUpdate { cell, _ in
scenesUpdateObserver = scenes.observe { _ in
cell.switchControl.setOn(scenes.filter(\.actionEnabled).count == scenes.count, animated: true)
}
}
toggleAllSwitch.updateCell()
form +++ Section(L10n.SettingsDetails.Actions.Scenes.title)
<<< toggleAllSwitch
form +++ RealmSection<RLMScene>(
footer: L10n.SettingsDetails.Actions.Scenes.footer,
collection: AnyRealmCollection(scenes),
emptyRows: [
LabelRow {
$0.title = L10n.SettingsDetails.Actions.Scenes.empty
$0.disabled = true
},
], getter: {
Self.getSceneRows($0)
}
)
case .privacy:
title = L10n.SettingsDetails.Privacy.title
form
+++ Section(header: nil, footer: L10n.SettingsDetails.Privacy.Messaging.description)
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Privacy.Messaging.title
$0.value = Current.settingsStore.privacy.messaging
}.onChange { row in
Current.settingsStore.privacy.messaging = row.value ?? true
Messaging.messaging().isAutoInitEnabled = Current.settingsStore.privacy.messaging
}
+++ Section(header: nil, footer: L10n.SettingsDetails.Privacy.Alerts.description)
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Privacy.Alerts.title
$0.value = Current.settingsStore.privacy.alerts
}.onChange { row in
Current.settingsStore.privacy.alerts = row.value ?? true
}
+++ Section(
header: nil,
footer: L10n.SettingsDetails.Privacy.CrashReporting.description
) {
$0.hidden = .init(booleanLiteral: !Current.crashReporter.hasCrashReporter)
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Privacy.CrashReporting.title
$0.value = Current.settingsStore.privacy.crashes
}.onChange { row in
Current.settingsStore.privacy.crashes = row.value ?? true
}
+++ Section(header: nil, footer: L10n.SettingsDetails.Privacy.Analytics.genericDescription) {
$0.hidden = .init(booleanLiteral: !Current.crashReporter.hasAnalytics)
}
<<< SwitchRow {
$0.title = L10n.SettingsDetails.Privacy.Analytics.genericTitle
$0.value = Current.settingsStore.privacy.analytics
}.onChange { row in
Current.settingsStore.privacy.analytics = row.value ?? true
}
default:
Current.Log.warning("Something went wrong, no settings detail group named \(detailGroup)")
}
}
override func tableView(_ tableView: UITableView, willBeginReorderingRowAtIndexPath indexPath: IndexPath) {
let row = form[indexPath]
guard let rowTag = row.tag else { return }
reorderingRows[rowTag] = row
super.tableView(tableView, willBeginReorderingRowAtIndexPath: indexPath)
}
private func updatePositions() {
guard let actionsSection = form.sectionBy(tag: "actions") as? MultivaluedSection else {
return
}
let rowsDict = actionsSection.allRows.enumerated().compactMap { entry -> (String, Int)? in
// Current.Log.verbose("Map \(entry.element.indexPath) \(entry.element.tag)")
guard let tag = entry.element.tag else { return nil }
return (tag, entry.offset)
}
let rowPositions = Dictionary(uniqueKeysWithValues: rowsDict)
realm.beginWrite()
for storedAction in realm.objects(Action.self).sorted(byKeyPath: "Position") {
guard let newPos = rowPositions[storedAction.ID] else { continue }
storedAction.Position = Action.PositionOffset.manual.rawValue + newPos
// Current.Log.verbose("Update action \(storedAction.ID) to pos \(newPos)")
}
try? realm.commitWrite()
}
@objc public func tableView(_ tableView: UITableView, didEndReorderingRowAtIndexPath indexPath: IndexPath) {
let row = form[indexPath]
Current.Log.verbose("Setting action \(row) to position \(indexPath.row)")
updatePositions()
reorderingRows[row.tag ?? ""] = nil
}
@objc func tableView(_ tableView: UITableView, didCancelReorderingRowAtIndexPath indexPath: IndexPath) {
guard let rowTag = form[indexPath].tag else { return }
reorderingRows[rowTag] = nil
}
override func rowsHaveBeenRemoved(_ rows: [BaseRow], at indexes: [IndexPath]) {
super.rowsHaveBeenRemoved(rows, at: indexes)
let deletedIDs = rows.filter {
guard let tag = $0.tag else { return false }
return reorderingRows[tag] == nil
}.compactMap(\.tag)
if deletedIDs.count == 0 { return }
Current.Log.verbose("Rows removed \(rows), \(deletedIDs)")
let realm = Realm.live()
if (rows.first as? ButtonRowWithPresent<ActionConfigurator>) != nil {
Current.Log.verbose("Removed row is ActionConfiguration \(deletedIDs)")
realm.reentrantWrite {
realm.delete(realm.objects(Action.self).filter("ID IN %@", deletedIDs))
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@objc func closeSettingsDetailView(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
private func resizeImage(image: UIImage?, newSize: CGSize) -> UIImage? {
guard let image else { return nil }
let renderer = UIGraphicsImageRenderer(size: newSize)
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
return resizedImage
}
static func getSceneRows(_ rlmScene: RLMScene) -> [BaseRow] {
let switchRow = SwitchRow()
let configure = ButtonRowWithPresent<ActionConfigurator> {
$0.title = L10n.SettingsDetails.Actions.Scenes.customizeAction
$0.disabled = .function([], { _ in switchRow.value == false })
$0.cellUpdate { cell, row in
cell.separatorInset = .zero
cell.textLabel?.textAlignment = .natural
cell.imageView?.image = UIImage(size: MaterialDesignIcons.settingsIconSize, color: .clear)
cell.textLabel?.textColor = row.isDisabled == false ? AppConstants.tintColor : .tertiaryLabel
}
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
ActionConfigurator(action: rlmScene.actions.first!)
}, onDismiss: { vc in
_ = vc.navigationController?.popViewController(animated: true)
if let vc = vc as? ActionConfigurator, vc.shouldSave, let realm = rlmScene.realm {
realm.reentrantWrite {
realm.add(vc.action, update: .all)
}
}
})
}
_ = with(switchRow) {
$0.title = rlmScene.name ?? rlmScene.identifier
$0.value = rlmScene.actionEnabled
$0.cellUpdate { cell, _ in
cell.imageView?.image =
rlmScene.icon
.flatMap({ MaterialDesignIcons(serversideValueNamed: $0) })?
.settingsIcon(for: cell.traitCollection)
}
$0.onChange { row in
do {
try rlmScene.realm?.write {
rlmScene.actionEnabled = row.value ?? true
}
configure.evaluateDisabled()
} catch {
Current.Log.error("couldn't write action update: \(error)")
}
}
}
return [switchRow, configure]
}
func getActionRow(_ inputAction: Action?) -> ButtonRowWithPresent<ActionConfigurator> {
var identifier = UUID().uuidString
var title = L10n.ActionsConfigurator.title
var action = inputAction
if let passedAction = inputAction {
identifier = passedAction.ID
title = passedAction.Name
}
return ButtonRowWithPresent<ActionConfigurator> {
$0.tag = identifier
$0.title = title
$0.cellStyle = .subtitle
$0.displayValueFor = { _ in
guard action == nil || action?.isInvalidated == false else { return nil }
return action?.Text ?? L10n.ActionsConfigurator.Rows.Text.title
}
$0.cellSetup { cell, _ in
cell.detailTextLabel?.textColor = .secondaryLabel
}
$0.cellUpdate { cell, _ in
guard action == nil || action?.isInvalidated == false else { return }
cell.imageView?.image = MaterialDesignIcons(named: action?.IconName ?? "")
.settingsIcon(for: cell.traitCollection)
}
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
ActionConfigurator(action: action)
}, onDismiss: { [weak self] vc in
_ = vc.navigationController?.popViewController(animated: true)
if let vc = vc as? ActionConfigurator {
defer {
if vc.shouldOpenAutomationEditor {
vc.navigationController?.dismiss(animated: true, completion: {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { controller in
controller.openActionAutomationEditor(actionId: vc.action.ID)
}
})
}
}
if vc.shouldSave == false {
Current.Log.verbose("Not saving action to DB and returning early!")
return
}
action = vc.action
vc.row.tag = vc.action.ID
vc.row.title = vc.action.Name
vc.row.updateCell()
Current.Log.verbose("Saving action! \(vc.action)")
let realm = Current.realm()
realm.reentrantWrite {
realm.add(vc.action, update: .all)
}.done {
self?.updatePositions()
}.cauterize()
}
})
}
}
private func locationPermissionsSection() -> Section {
let section = Section()
section <<< locationPermissionRow()
section <<< locationAccuracyRow()
section <<< backgroundRefreshRow()
return section
}
private func locationPermissionRow() -> BaseRow {
class PermissionWatchingDelegate: NSObject, CLLocationManagerDelegate {
let row: LocationPermissionRow
init(row: LocationPermissionRow) {
self.row = row
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
row.value = manager.authorizationStatus
row.updateCell()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
row.value = status
row.updateCell()
}
}
return LocationPermissionRow("locationPermission") {
let locationManager = CLLocationManager()
let permissionDelegate = PermissionWatchingDelegate(row: $0)
$0.title = L10n.SettingsDetails.Location.LocationPermission.title
$0.cellUpdate { cell, _ in
// setting the delegate also has the side effect of triggering a status update, which sets the value
locationManager.delegate = permissionDelegate
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
}
$0.onCellSelection { _, row in
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestAlwaysAuthorization()
} else {
UIApplication.shared.openSettings(destination: .location)
}
row.deselect(animated: true)
}
}
}
private func locationAccuracyRow() -> BaseRow {
class PermissionWatchingDelegate: NSObject, CLLocationManagerDelegate {
let row: LocationAccuracyRow
init(row: LocationAccuracyRow) {
self.row = row
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
row.value = manager.accuracyAuthorization
row.updateCell()
}
}
return LocationAccuracyRow("locationAccuracy") {
let locationManager = CLLocationManager()
let permissionDelegate = PermissionWatchingDelegate(row: $0)
$0.title = L10n.SettingsDetails.Location.LocationAccuracy.title
$0.cellUpdate { cell, _ in
// setting the delegate also has the side effect of triggering a status update, which sets the value
locationManager.delegate = permissionDelegate
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
}
$0.onCellSelection { _, row in
UIApplication.shared.openSettings(destination: .location)
row.deselect(animated: true)
}
}
}
private func backgroundRefreshRow() -> BaseRow {
BackgroundRefreshStatusRow("backgroundRefresh") { row in
func updateRow(isInitial: Bool) {
row.value = UIApplication.shared.backgroundRefreshStatus
if !isInitial {
row.updateCell()
}
}
notificationCenterTokens.append(NotificationCenter.default.addObserver(
forName: UIApplication.backgroundRefreshStatusDidChangeNotification,
object: nil,
queue: .main
) { _ in
updateRow(isInitial: false)
})
updateRow(isInitial: true)
row.hidden = .isCatalyst
row.title = L10n.SettingsDetails.Location.BackgroundRefresh.title
row.cellUpdate { cell, _ in
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
}
row.onCellSelection { _, row in
UIApplication.shared.openSettings(destination: .backgroundRefresh)
row.deselect(animated: true)
}
}
}
}
enum AppIcon: String, CaseIterable {
case Release = "release"
case Beta = "beta"
case Dev = "dev"
case Black = "black"
case Blue = "blue"
case CaribbeanGreen = "caribbean-green"
case CornflowerBlue = "cornflower-blue"
case Crimson = "crimson"
case ElectricViolet = "electric-violet"
case FireOrange = "fire-orange"
case Green = "green"
case Classic = "classic"
case OldBeta = "old-beta"
case OldDev = "old-dev"
case OldRelease = "old-release"
case Orange = "orange"
case Pink = "pink"
case Purple = "purple"
case Red = "red"
case White = "white"
case BiPride = "bi_pride"
case POCPride = "POC_pride"
case NonBinary = "non-binary"
case Rainbow = "rainbow"
case Trans = "trans"
var title: String {
switch self {
case .Release:
return L10n.SettingsDetails.General.AppIcon.Enum.release
case .Beta:
return L10n.SettingsDetails.General.AppIcon.Enum.beta
case .Dev:
return L10n.SettingsDetails.General.AppIcon.Enum.dev
case .Black:
return L10n.SettingsDetails.General.AppIcon.Enum.black
case .Blue:
return L10n.SettingsDetails.General.AppIcon.Enum.blue
case .CaribbeanGreen:
return L10n.SettingsDetails.General.AppIcon.Enum.caribbeanGreen
case .CornflowerBlue:
return L10n.SettingsDetails.General.AppIcon.Enum.cornflowerBlue
case .Crimson:
return L10n.SettingsDetails.General.AppIcon.Enum.crimson
case .ElectricViolet:
return L10n.SettingsDetails.General.AppIcon.Enum.electricViolet
case .FireOrange:
return L10n.SettingsDetails.General.AppIcon.Enum.fireOrange
case .Green:
return L10n.SettingsDetails.General.AppIcon.Enum.green
case .Classic:
return L10n.SettingsDetails.General.AppIcon.Enum.classic
case .OldBeta:
return L10n.SettingsDetails.General.AppIcon.Enum.oldBeta
case .OldDev:
return L10n.SettingsDetails.General.AppIcon.Enum.oldDev
case .OldRelease:
return L10n.SettingsDetails.General.AppIcon.Enum.oldRelease
case .Orange:
return L10n.SettingsDetails.General.AppIcon.Enum.orange
case .Pink:
return L10n.SettingsDetails.General.AppIcon.Enum.pink
case .Purple:
return L10n.SettingsDetails.General.AppIcon.Enum.purple
case .Red:
return L10n.SettingsDetails.General.AppIcon.Enum.red
case .White:
return L10n.SettingsDetails.General.AppIcon.Enum.white
case .BiPride:
return L10n.SettingsDetails.General.AppIcon.Enum.prideBi
case .POCPride:
return L10n.SettingsDetails.General.AppIcon.Enum.pridePoc
case .Rainbow:
return L10n.SettingsDetails.General.AppIcon.Enum.prideRainbow
case .Trans:
return L10n.SettingsDetails.General.AppIcon.Enum.prideTrans
case .NonBinary:
return L10n.SettingsDetails.General.AppIcon.Enum.prideNonBinary
}
}
var isDefault: Bool {
switch Current.appConfiguration {
case .debug where self == .Dev: return true
case .beta where self == .Beta: return true
case .release where self == .Release: return true
default: return false
}
}
var iconName: String? {
if isDefault {
return nil
} else {
return rawValue
}
}
}
enum OpenInBrowser: String, CaseIterable {
case Chrome
case Firefox
case FirefoxFocus
case FirefoxKlar
case Safari
case SafariInApp
var title: String {
switch self {
case .Chrome:
return L10n.SettingsDetails.General.OpenInBrowser.chrome
case .Firefox:
return L10n.SettingsDetails.General.OpenInBrowser.firefox
case .FirefoxFocus:
return L10n.SettingsDetails.General.OpenInBrowser.firefoxFocus
case .FirefoxKlar:
return L10n.SettingsDetails.General.OpenInBrowser.firefoxKlar
case .Safari:
return L10n.SettingsDetails.General.OpenInBrowser.default
case .SafariInApp:
return L10n.SettingsDetails.General.OpenInBrowser.safariInApp
}
}
var isInstalled: Bool {
switch self {
case .Chrome:
return OpenInChromeController.sharedInstance.isChromeInstalled()
case .Firefox:
return OpenInFirefoxControllerSwift().isFirefoxInstalled()
case .FirefoxFocus:
return OpenInFirefoxControllerSwift(type: .focus).isFirefoxInstalled()
case .FirefoxKlar:
return OpenInFirefoxControllerSwift(type: .klar).isFirefoxInstalled()
default:
return true
}
}
var supportsPrivateTabs: Bool {
switch self {
case .Firefox:
return true
default:
return false
}
}
}