remindwall / RWPureSwift / Sources / EditSettingsNew / TopLevel / ScreenOffSetting.swift
Back to remindwall — Current RemindWall
import AppTypes
import ComposableArchitecture
import Dao
import os
import ScreenControl
import SwiftUI
private let logger = Logger(subsystem: "RemindWall", category: "ScreenOffSetting")
@Reducer
public struct ScreenOffSettingFeature {
@Dependency(\.continuousClock) var clock
@Dependency(\.screenControl) var screenControl
@ObservableState
public struct State: Equatable {
@Shared(.syncedSetting(SCREEN_OFF_SETTING_KEY)) var schedule: ScreenOffSchedule?
public var isTesting: Bool = false
public var testStatus: String?
public init() {}
}
public enum Action {
case setStartTime(hour: Int, minute: Int)
case setEndTime(hour: Int, minute: Int)
case setSchedule(startHour: Int, startMinute: Int, endHour: Int, endMinute: Int)
case testScreenOff
case _testProgress(String)
case _testComplete
}
public init() {}
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .setStartTime(hour, minute):
guard let s = state.schedule else { return .none }
let clampedHour = ((hour % 24) + 24) % 24
let clampedMinute = ((minute % 60) + 60) % 60
state.$schedule.withLock {
$0 = ScreenOffSchedule(startHour: clampedHour, startMinute: clampedMinute, endHour: s.endHour, endMinute: s.endMinute)
}
return .none
case let .setEndTime(hour, minute):
guard let s = state.schedule else { return .none }
let clampedHour = ((hour % 24) + 24) % 24
let clampedMinute = ((minute % 60) + 60) % 60
state.$schedule.withLock {
$0 = ScreenOffSchedule(startHour: s.startHour, startMinute: s.startMinute, endHour: clampedHour, endMinute: clampedMinute)
}
return .none
case let .setSchedule(startHour, startMinute, endHour, endMinute):
guard state.schedule != nil else { return .none }
state.$schedule.withLock {
$0 = ScreenOffSchedule(
startHour: ((startHour % 24) + 24) % 24,
startMinute: ((startMinute % 60) + 60) % 60,
endHour: ((endHour % 24) + 24) % 24,
endMinute: ((endMinute % 60) + 60) % 60
)
}
return .none
case .testScreenOff:
guard !state.isTesting else { return .none }
state.isTesting = true
state.testStatus = "Saving brightness…"
return .run { [screenControl, clock] send in
let savedBrightness = try await screenControl.getBrightness()
await send(._testProgress("Dimming screen…"))
try await screenControl.setBrightness(0.0)
for remaining in stride(from: 10, through: 1, by: -1) {
await send(._testProgress("Screen off — restoring in \(remaining)s"))
try await clock.sleep(for: .seconds(1))
}
await send(._testProgress("Restoring brightness…"))
try await screenControl.setBrightness(savedBrightness)
await send(._testComplete)
} catch: { [screenControl] error, send in
// Never leave the test stranded dark: full-brightness rescue.
// (savedBrightness isn't in scope here; 1.0 is the safe exit
// for a hand-run test button.)
logger.warning("screen off test failed: \(error.localizedDescription, privacy: .public)")
try? await screenControl.setBrightness(1.0)
await send(._testComplete)
}
case let ._testProgress(status):
state.testStatus = status
return .none
case ._testComplete:
state.isTesting = false
state.testStatus = nil
return .none
}
}
}
}
public struct ScreenOffSettingView: View {
let store: StoreOf<ScreenOffSettingFeature>
public init(store: StoreOf<ScreenOffSettingFeature>) {
self.store = store
}
public var body: some View {
if let schedule = store.schedule {
VStack(spacing: 16) {
ClockDialView(
startHour: schedule.startHour,
startMinute: schedule.startMinute,
endHour: schedule.endHour,
endMinute: schedule.endMinute,
onStartTimeChanged: { hour, minute in
store.send(.setStartTime(hour: hour, minute: minute))
},
onEndTimeChanged: { hour, minute in
store.send(.setEndTime(hour: hour, minute: minute))
},
onBothTimesChanged: { sh, sm, eh, em in
store.send(.setSchedule(startHour: sh, startMinute: sm, endHour: eh, endMinute: em))
}
)
.frame(height: 300)
.padding(.horizontal)
HStack(spacing: 32) {
timeLabel(
icon: "moon.fill",
iconColor: .indigo,
label: "SCREEN OFF",
display: schedule.startTimeDisplay
)
timeLabel(
icon: "sun.max.fill",
iconColor: .orange,
label: "SCREEN ON",
display: schedule.endTimeDisplay
)
}
Button {
store.send(.testScreenOff)
} label: {
HStack {
if store.isTesting {
ProgressView()
.padding(.trailing, 4)
Text(store.testStatus ?? "Testing…")
} else {
Image(systemName: "display")
Text("Test Screen Off")
}
}
}
.disabled(store.isTesting)
}
.padding(.vertical, 8)
}
}
@ViewBuilder
private func timeLabel(
icon: String,
iconColor: Color,
label: String,
display: String
) -> some View {
VStack(spacing: 4) {
HStack(spacing: 4) {
Image(systemName: icon)
.foregroundStyle(iconColor)
.font(.caption)
Text(label)
.font(.caption2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
}
Text(display)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.monospacedDigit()
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier(label)
}
}
#Preview {
NavigationStack {
Form {
ScreenOffSettingView(
store: Store(
initialState: {
let state = ScreenOffSettingFeature.State()
state.$schedule.withLock { $0 = .default }
return state
}()
) {
ScreenOffSettingFeature()
}
)
}
}
}