remindwall / RWPureSwift / Sources / EditSettingsNew / TopLevel / Settings.swift
Back to remindwall — Current RemindWall
import AppTypes
import ComposableArchitecture
import EditSettingsNew_BusSettings
import EditSettingsNew_Trackees
import ScreenControl
import SwiftUI
private enum TerminateAppKey: DependencyKey {
static let liveValue: @Sendable () -> Void = { exit(0) }
static let testValue: @Sendable () -> Void = { }
}
extension DependencyValues {
var terminateApp: @Sendable () -> Void {
get { self[TerminateAppKey.self] }
set { self[TerminateAppKey.self] = newValue }
}
}
@Reducer
public struct SettingsFeature {
@Dependency(\.continuousClock) var clock
@Dependency(\.fireAndForget) var fireAndForget
@Dependency(\.screenControl) var screenControl
@Dependency(\.terminateApp) var terminateApp
@ObservableState
public struct State: Equatable {
public var trackeesState = TrackeesFeature.State()
public var albumPickerState = AlbumPickerFeature.State()
public var calendarPickerState = CalendarPickerFeature.State()
public var screenOffSettingState = ScreenOffSettingFeature.State()
public var alertsState = AlertsSettingsFeature.State()
public var path = StackState<TrackeeDetailFeature.State>()
public var isBrightnessControlAvailable: Bool = true
public var batterySettingsState = BatterySettingsFeature.State()
public init(){}
}
public enum Action {
case albumPicker(AlbumPickerFeature.Action)
case batteryAlertsToggled(Bool)
case batterySettings(BatterySettingsFeature.Action)
case alerts(AlertsSettingsFeature.Action)
case calendarPicker(CalendarPickerFeature.Action)
case delegate(Delegate)
case screenOffSetting(ScreenOffSettingFeature.Action)
case trackees(TrackeesFeature.Action)
case path(StackActionOf<TrackeeDetailFeature>)
case _brightnessCheckCompleted(Bool)
case calendarToggled(Bool)
case onAppear
case onDisappear
case quitApplication
case screenOffToggled(Bool)
case slideshowToggled(Bool)
case startSlideshow
@CasePathable
public enum Delegate: Equatable {
case startSlideshow
}
}
enum CancelID { case availabilityLoop }
public init() {}
public var body: some ReducerOf<Self> {
Scope(state: \.albumPickerState, action: \.albumPicker) {
AlbumPickerFeature()
}
Scope(state: \.batterySettingsState, action: \.batterySettings) {
BatterySettingsFeature()
}
Scope(state: \.alertsState, action: \.alerts) {
AlertsSettingsFeature()
}
Scope(state: \.calendarPickerState, action: \.calendarPicker) {
CalendarPickerFeature()
}
Scope(state: \.screenOffSettingState, action: \.screenOffSetting) {
ScreenOffSettingFeature()
}
Scope(state: \.trackeesState, action: \.trackees) {
TrackeesFeature()
}
Reduce { state, action in
switch action {
case let .batteryAlertsToggled(isOn):
state.batterySettingsState.$enabled.withLock { $0 = isOn }
return .none
case let .calendarToggled(isOn):
if isOn {
state.calendarPickerState.$selectedCalendar.withLock { $0 = CalendarId("") }
} else {
state.calendarPickerState.$selectedCalendar.withLock { $0 = nil }
}
return .none
case .onAppear:
// Poll while settings is visible so the warning clears live as
// the operator fixes the daemon; cancelled on disappear (the
// dashboard runs 24/7 - a leaked poll would run forever).
return .run { [screenControl, clock] send in
await send(._brightnessCheckCompleted(screenControl.isAvailable()))
for await _ in clock.timer(interval: .seconds(15)) {
await send(._brightnessCheckCompleted(screenControl.isAvailable()))
}
}
.cancellable(id: CancelID.availabilityLoop, cancelInFlight: true)
case .onDisappear:
return .cancel(id: CancelID.availabilityLoop)
case let ._brightnessCheckCompleted(available):
state.isBrightnessControlAvailable = available
return .none
case .quitApplication:
return .run { [fireAndForget, terminateApp] _ in
await fireAndForget {
terminateApp()
}
}
case let .screenOffToggled(isOn):
if isOn {
state.screenOffSettingState.$schedule.withLock { $0 = .default }
} else {
state.screenOffSettingState.$schedule.withLock { $0 = nil }
}
return .none
case let .slideshowToggled(isOn):
if isOn {
state.albumPickerState.$selectedAlbum.withLock { $0 = AlbumLocalId("") }
} else {
state.albumPickerState.$selectedAlbum.withLock { $0 = nil }
}
return .none
case .startSlideshow:
return .send(.delegate(.startSlideshow))
case .delegate:
return .none
case let .path(.element(id: id, action: .delegate(.confirmDeletion))):
guard let detailState = state.path[id: id]
else { return .none }
return .send(.trackees(.deleteTrackee(detailState.trackee.id)))
case .alerts, .batterySettings, .trackees, .albumPicker, .calendarPicker, .screenOffSetting, .path:
return .none
}
}.forEach(\.path, action: \.path) {
TrackeeDetailFeature()
}
}
}
public struct SettingsView: View {
@Bindable var store: StoreOf<SettingsFeature>
public init(store: StoreOf<SettingsFeature>) {
self.store = store
}
public var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
Form{
Section {
if store.albumPickerState.selectedAlbum != nil {
AlbumPickerView(store: store.scope(state: \.albumPickerState, action: \.albumPicker))
}
} header: {
Toggle("Slideshow", isOn: Binding(
get: { store.albumPickerState.selectedAlbum != nil },
set: { store.send(.slideshowToggled($0)) }
))
}
Section {
if store.calendarPickerState.selectedCalendar != nil {
CalendarPickerView(store: store.scope(state: \.calendarPickerState, action: \.calendarPicker))
}
} header: {
Toggle("Calendar Reminders", isOn: Binding(
get: { store.calendarPickerState.selectedCalendar != nil },
set: { store.send(.calendarToggled($0)) }
))
}
Section {
if store.screenOffSettingState.schedule != nil {
ScreenOffSettingView(
store: store.scope(state: \.screenOffSettingState, action: \.screenOffSetting)
)
}
#if targetEnvironment(macCatalyst)
if !store.isBrightnessControlAvailable {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.yellow)
Text("Screen control daemon (ddcd) unreachable — brightness and panel power are disabled. Setup: scripts/kiosk/INSTALL.md in the repo")
.font(.caption)
.foregroundStyle(.secondary)
}
}
#endif
} header: {
Toggle("Screen Off", isOn: Binding(
get: { store.screenOffSettingState.schedule != nil },
set: { store.send(.screenOffToggled($0)) }
))
}
Section {
AlertsSettingsView(
store: store.scope(state: \.alertsState, action: \.alerts)
)
} header: {
Text("Alerts")
}
Section {
if store.batterySettingsState.enabled {
BatterySettingsView(
store: store.scope(state: \.batterySettingsState, action: \.batterySettings)
)
}
} header: {
Toggle("Battery Alerts", isOn: Binding(
get: { store.batterySettingsState.enabled },
set: { store.send(.batteryAlertsToggled($0)) }
))
}
Section {
TrackeesView(store: store.scope(state: \.trackeesState, action: \.trackees), isEmbedded: true)
} header: {
HStack {
Text("Trackees")
Spacer()
Button {
store.send(.trackees(.addButtonTapped))
} label: {
Image(systemName: "plus")
}
}
}
Section {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown"
LabeledContent("Version") {
Text("\(version) (\(build))")
}
}
#if targetEnvironment(macCatalyst)
Section {
Button("Quit Application", role: .destructive) {
store.send(.quitApplication)
}
}
#endif
}
.onAppear { store.send(.onAppear) }
.onDisappear { store.send(.onDisappear) }
.navigationTitle("Settings")
.toolbar {
ToolbarItem {
Button("Start Slideshow") {
store.send(.startSlideshow)
}
}
}
} destination: { store in
TrackeeDetailView(store: store)
}
}
}
#Preview {
let _ = prepareDependencies {
$0.defaultDatabase = try! $0.appDatabase()
}
SettingsView(
store: Store(
initialState: SettingsFeature.State()
) {
SettingsFeature()
}
)
}