Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Sources / EditSettingsNew / TopLevel / ScreenOffSetting.swift

Back to remindwall — Current RemindWall

Download this file

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()
                }
            )
        }
    }
}