Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Sources / EditSettingsNew / BusSettings / WatchDetail.swift

Back to remindwall — Current RemindWall

Download this file

import AppTypes
import ComposableArchitecture
import Dao
import Dependencies
import Foundation
import SwiftUI

@Reducer
public struct WatchDetailFeature: Sendable {
    @Dependency(\.defaultDatabase) var defaultDatabase
    @Dependency(\.dismiss) var dismiss

    @ObservableState
    public struct State: Equatable {
        public var watch: Watch

        public init(watch: Watch) {
            self.watch = watch
        }
    }

    public enum Action: Equatable {
        case enabledToggled(Bool)
        case setWindowStart(hour: Int, minute: Int)
        case setWindowEnd(hour: Int, minute: Int)
        case toggleWeekday(DaysOfWeek)
        case normalMinutesChanged(Int)
        case removeTapped
        case doneTapped
        case delegate(Delegate)

        @CasePathable
        public enum Delegate: Equatable {
            case watchChanged
            case removeRequested(Watch.ID)
        }
    }

    public init() {}

    public var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case let .enabledToggled(isOn):
                switch state.watch {
                case var .bus(stop):
                    stop.enabled = isOn
                    state.watch = .bus(stop)
                case var .drive(route):
                    route.enabled = isOn
                    state.watch = .drive(route)
                }
                return persist(state.watch)

            case let .setWindowStart(hour, minute):
                return updateWindow(&state) { $0.withStart(hour: hour, minute: minute) }

            case let .setWindowEnd(hour, minute):
                return updateWindow(&state) { $0.withEnd(hour: hour, minute: minute) }

            case let .toggleWeekday(day):
                return updateWindow(&state) { current in
                    var days = current.weekdays
                    if days.contains(day) { days.remove(day) } else { days.insert(day) }
                    return current.withWeekdays(days)
                }

            case let .normalMinutesChanged(minutes):
                guard case var .drive(route) = state.watch else { return .none }
                route.normalMinutes = min(max(minutes, 1), 240)
                state.watch = .drive(route)
                return persist(state.watch)

            case .removeTapped:
                return .send(.delegate(.removeRequested(state.watch.id)))

            case .doneTapped:
                return .run { _ in await dismiss() }

            case .delegate:
                return .none
            }
        }
    }

    private func updateWindow(
        _ state: inout State,
        _ transform: (AlertWindow) -> AlertWindow
    ) -> Effect<Action> {
        let newWindow = transform(state.watch.window ?? .default)
        switch state.watch {
        case var .bus(stop):
            stop.window = newWindow
            state.watch = .bus(stop)
        case var .drive(route):
            route.window = newWindow
            state.watch = .drive(route)
        }
        return persist(state.watch)
    }

    /// The detail owns its row's writes; the parent reloads on watchChanged
    /// (sent AFTER the commit so the reload never reads stale).
    private func persist(_ watch: Watch) -> Effect<Action> {
        .run { send in
            await withErrorReporting {
                try await defaultDatabase.write { db in
                    switch watch {
                    case let .bus(stop):
                        try MonitoredStop.find(stop.id)
                            .update {
                                $0.enabled = stop.enabled
                                $0.window = stop.window
                            }
                            .execute(db)
                    case let .drive(route):
                        try MonitoredRoute.find(route.id)
                            .update {
                                $0.enabled = route.enabled
                                $0.window = route.window
                                $0.normalMinutes = route.normalMinutes
                            }
                            .execute(db)
                    }
                }
                await send(.delegate(.watchChanged))
            }
        }
    }
}

public struct WatchDetailView: View {
    @Bindable var store: StoreOf<WatchDetailFeature>

    public init(store: StoreOf<WatchDetailFeature>) {
        self.store = store
    }

    public var body: some View {
        NavigationStack {
            Form {
                Section("Watch") {
                    Toggle("Enabled", isOn: Binding(
                        get: { store.watch.enabled },
                        set: { store.send(.enabledToggled($0)) }
                    ))
                    LabeledContent("What") { Text(store.watch.label) }
                    LabeledContent("Where") { Text(store.watch.whereText) }
                    if case let .drive(route) = store.watch {
                        Stepper(
                            "Normal drive: \(route.normalMinutes) min",
                            value: Binding(
                                get: { route.normalMinutes },
                                set: { store.send(.normalMinutesChanged($0)) }
                            ),
                            in: 1...240,
                            step: 1
                        )
                    }
                }
                Section("When it matters") {
                    AlertWindowEditorView(
                        window: store.watch.window ?? .default,
                        onSetStart: { hour, minute in store.send(.setWindowStart(hour: hour, minute: minute)) },
                        onSetEnd: { hour, minute in store.send(.setWindowEnd(hour: hour, minute: minute)) },
                        onToggleWeekday: { day in store.send(.toggleWeekday(day)) }
                    )
                }
                Section {
                    Button("Remove this alert", role: .destructive) {
                        store.send(.removeTapped)
                    }
                }
            }
            .navigationTitle(store.watch.label)
            .toolbar {
                ToolbarItem {
                    Button("Done") { store.send(.doneTapped) }
                }
            }
        }
    }
}