diff --git a/Sources/App/Settings/DebugView.swift b/Sources/App/Settings/DebugView.swift index b0f8df1cd..f20b80e1f 100644 --- a/Sources/App/Settings/DebugView.swift +++ b/Sources/App/Settings/DebugView.swift @@ -145,21 +145,6 @@ struct DebugView: View { #endif } - #if os(iOS) && !targetEnvironment(macCatalyst) - if #available(iOS 17.2, *) { - Section { - NavigationLink { - LiveActivitySettingsView() - } label: { - linkContent( - image: .init(systemSymbol: .livephoto), - title: L10n.LiveActivity.title - ) - } - } - } - #endif - criticalSection if tapsOnCasitaLogo < 10 { diff --git a/Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift b/Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift index 97be42038..7c564fa7c 100644 --- a/Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift +++ b/Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift @@ -27,6 +27,7 @@ struct LiveActivitySettingsView: View { ) statusSection + frequentUpdatesSection if activities.isEmpty { Section(L10n.LiveActivity.Section.active) { @@ -62,15 +63,8 @@ struct LiveActivitySettingsView: View { } } - #if DEBUG - debugSection - #endif - privacySection - - if #available(iOS 17.2, *) { - frequentUpdatesSection - } + samplesSection } .navigationTitle(L10n.LiveActivity.title) .task { await loadActivities() } @@ -101,7 +95,7 @@ struct LiveActivitySettingsView: View { } } - // MARK: - Debug (DEBUG builds only) + // MARK: - Samples // // Two sections: Static (fixed snapshots to verify layout) and Animated (multi-stage @@ -121,131 +115,139 @@ struct LiveActivitySettingsView: View { // It does NOT appear on the lock screen. Use a Dynamic Island device or // simulator (iPhone 14 Pro+) to see it. - #if DEBUG - private var debugSection: some View { - Group { - Section { - // Minimum viable layout — only the message field is set. - // Verifies the bare layout renders without icon, progress, or timer. - Button("Plain Message") { - startTestActivity( - tag: "debug-plain", - title: "Home Assistant", - state: .init(message: "Everything looks good at home.") - ) + private var samplesSection: some View { + Section { + NavigationLink("Samples") { + List { + staticSamplesSection + animatedSamplesSection } - - // icon = nil code path. Layout must not shift or break when no icon is provided. - // color = nil so the progress bar uses the default HA-blue tint. - // criticalText ("Active") visible in DI compact trailing only. - Button("No Icon · Default Color") { - startTestActivity( - tag: "debug-no-icon", - title: "Script Running", - state: .init( - message: "Irrigation zone 3 is active", - criticalText: "Active", - progress: 35, - progressMax: 100 - ) - ) - } - - // Short 60-second countdown with no progress bar. - // Red color communicates urgency. Watch the timer count down in real time. - // Represents automations like alarm arming delays or reminder countdowns. - Button("Alarm · 60 sec Countdown") { - startTestActivity( - tag: "debug-alarm", - title: "Security Alarm", - state: .init( - message: "Motion at back door · Arms in 60 seconds", - criticalText: "60 sec", - chronometer: true, - countdownEnd: Date().addingTimeInterval(60), - icon: "mdi:alarm-light", - color: "#F44336" - ) - ) - } - - // Every ContentState field active at the same time. - // Lock screen shows: icon → live countdown → progress bar. - // criticalText ("5 min") visible in DI compact trailing only. - // Use this to confirm no layout collisions when all fields are populated. - Button("All Fields · Max Load") { - startTestActivity( - tag: "debug-all", - title: "All Fields", - state: .init( - message: "All content state fields active", - criticalText: "5 min", - progress: 42, - progressMax: 100, - chronometer: true, - countdownEnd: Date().addingTimeInterval(5 * 60), - icon: "mdi:home-assistant", - color: "#03A9F4" - ) - ) - } - } header: { - Text("Debug · Static") - } footer: { - Text("Fixed state — no updates after start. Good for checking layout at a glance.") - } - - Section { - // Progress bar advances through five named stages. - // criticalText tracks the current stage name in the DI compact trailing slot. - // Icon swaps from washing-machine to check-circle on the final update. - // Represents any multi-step appliance cycle automation. - Button("Washing Machine · Stage Labels (~12 s)") { startWashingMachineCycle() } - - // Numeric percentage in criticalText updates alongside the progress bar. - // Color shifts from green to yellow-green as the charge nears 100 %. - // Represents any "% complete with time remaining" automation pattern. - Button("EV Charging · Numeric % (~16 s)") { startEVChargingSimulation() } - - // The only scenario where both progress (playback position) and a live countdown - // (time remaining in track) are active and updating at the same time. - // Simulates a track change mid-sequence: progress resets, countdown resets. - Button("Media Player · Progress + Timer (~20 s)") { startMediaNowPlaying() } - - // Message, criticalText, and icon all change on every update — no progress bar. - // Represents automations where the status category itself changes (not just a value). - Button("Package Delivery · All Text Fields (~15 s)") { startPackageJourney() } - - // No progress bar — state communicated entirely through color and icon. - // Escalates orange (motion) → red (person) → green (all clear). - // Represents any alert-and-resolve automation pattern. - Button("Security Escalation · Color + Icon (~8 s)") { startSecuritySequence() } - - // Cycles through wash stages then calls activity.end() with .default dismissal. - // The only scenario that tests the full lifecycle: start → update → end. - // After ending, the final "Done" state lingers on the lock screen (up to 4 h). - Button("Dishwasher · Full Lifecycle, Ends Itself (~12 s)") { startDishwasherAutoComplete() } - - // Fires 6 updates 2 seconds apart (12 s total). - // On iOS 18 the system enforces ~15 s between rendered updates — some will be - // silently dropped. Watch the counter skip values to see the rate limit in action. - // On the simulator and iOS 17 all 6 updates should render. - Button("Rate Limit · 6 Rapid Updates, 2 s Apart (~12 s)") { startRapidUpdateStressTest() } - } header: { - Text("Debug · Animated") - } footer: { - Text( - "Activity updates itself after you tap. Tap, then immediately lock (⌘L) " + - "to watch updates on the lock screen in real time." - ) + .navigationTitle("Samples") } } } - #endif - #if DEBUG + private var staticSamplesSection: some View { + Section { + // Minimum viable layout — only the message field is set. + // Verifies the bare layout renders without icon, progress, or timer. + Button("Plain Message") { + startTestActivity( + tag: "debug-plain", + title: "Home Assistant", + state: .init(message: "Everything looks good at home.") + ) + } - // MARK: - Debug helpers + // icon = nil code path. Layout must not shift or break when no icon is provided. + // color = nil so the progress bar uses the default HA-blue tint. + // criticalText ("Active") visible in DI compact trailing only. + Button("No Icon · Default Color") { + startTestActivity( + tag: "debug-no-icon", + title: "Script Running", + state: .init( + message: "Irrigation zone 3 is active", + criticalText: "Active", + progress: 35, + progressMax: 100 + ) + ) + } + + // Short 60-second countdown with no progress bar. + // Red color communicates urgency. Watch the timer count down in real time. + // Represents automations like alarm arming delays or reminder countdowns. + Button("Alarm · 60 sec Countdown") { + startTestActivity( + tag: "debug-alarm", + title: "Security Alarm", + state: .init( + message: "Motion at back door · Arms in 60 seconds", + criticalText: "60 sec", + chronometer: true, + countdownEnd: Date().addingTimeInterval(60), + icon: "mdi:alarm-light", + color: "#F44336" + ) + ) + } + + // Every ContentState field active at the same time. + // Lock screen shows: icon → live countdown → progress bar. + // criticalText ("5 min") visible in DI compact trailing only. + // Use this to confirm no layout collisions when all fields are populated. + Button("All Fields · Max Load") { + startTestActivity( + tag: "debug-all", + title: "All Fields", + state: .init( + message: "All content state fields active", + criticalText: "5 min", + progress: 42, + progressMax: 100, + chronometer: true, + countdownEnd: Date().addingTimeInterval(5 * 60), + icon: "mdi:home-assistant", + color: "#03A9F4" + ) + ) + } + } header: { + Text("Sample · Static") + } footer: { + Text("Fixed state — no updates after start. Good for checking layout at a glance.") + } + } + + private var animatedSamplesSection: some View { + Section { + // Progress bar advances through five named stages. + // criticalText tracks the current stage name in the DI compact trailing slot. + // Icon swaps from washing-machine to check-circle on the final update. + // Represents any multi-step appliance cycle automation. + Button("Washing Machine · Stage Labels (~12 s)") { startWashingMachineCycle() } + + // Numeric percentage in criticalText updates alongside the progress bar. + // Color shifts from green to yellow-green as the charge nears 100 %. + // Represents any "% complete with time remaining" automation pattern. + Button("EV Charging · Numeric % (~16 s)") { startEVChargingSimulation() } + + // The only scenario where both progress (playback position) and a live countdown + // (time remaining in track) are active and updating at the same time. + // Simulates a track change mid-sequence: progress resets, countdown resets. + Button("Media Player · Progress + Timer (~20 s)") { startMediaNowPlaying() } + + // Message, criticalText, and icon all change on every update — no progress bar. + // Represents automations where the status category itself changes (not just a value). + Button("Package Delivery · All Text Fields (~15 s)") { startPackageJourney() } + + // No progress bar — state communicated entirely through color and icon. + // Escalates orange (motion) → red (person) → green (all clear). + // Represents any alert-and-resolve automation pattern. + Button("Security Escalation · Color + Icon (~8 s)") { startSecuritySequence() } + + // Cycles through wash stages then calls activity.end() with .default dismissal. + // The only scenario that tests the full lifecycle: start → update → end. + // After ending, the final "Done" state lingers on the lock screen (up to 4 h). + Button("Dishwasher · Full Lifecycle, Ends Itself (~12 s)") { startDishwasherAutoComplete() } + + // Fires 6 updates 2 seconds apart (12 s total). + // On iOS 18 the system enforces ~15 s between rendered updates — some will be + // silently dropped. Watch the counter skip values to see the rate limit in action. + // On the simulator and iOS 17 all 6 updates should render. + Button("Rate Limit · 6 Rapid Updates, 2 s Apart (~12 s)") { startRapidUpdateStressTest() } + } header: { + Text("Sample · Animated") + } footer: { + Text( + "Activity updates itself after you tap. Tap, then immediately lock (⌘L) " + + "to watch updates on the lock screen in real time." + ) + } + } + + // MARK: - Sample helpers /// Starts a single-state activity (no subsequent updates). private func startTestActivity(tag: String, title: String, state: HALiveActivityAttributes.ContentState) { @@ -565,8 +567,6 @@ struct LiveActivitySettingsView: View { ) } - #endif - private var privacySection: some View { Section { Label(L10n.LiveActivity.Privacy.message, systemSymbol: .lockShield) diff --git a/Sources/App/Settings/Settings/SettingsItem.swift b/Sources/App/Settings/Settings/SettingsItem.swift index 4e183efab..34eb5180b 100644 --- a/Sources/App/Settings/Settings/SettingsItem.swift +++ b/Sources/App/Settings/Settings/SettingsItem.swift @@ -149,22 +149,30 @@ enum SettingsItem: String, Hashable, CaseIterable { allCases.filter { item in // Filter based on platform #if targetEnvironment(macCatalyst) - if item == .servers || item == .gestures || item == .kiosk || item == .watch || item == .carPlay || - item == .complications || item == .nfc || item == .help || - item == .whatsNew { + let hiddenItems: [SettingsItem] = [ + .servers, + .gestures, + .kiosk, + .watch, + .carPlay, + .complications, + .nfc, + .help, + .whatsNew, + .liveActivities, + ] + + if hiddenItems.contains(item) { return false } #endif - // Live Activities are shown in DebugView - if item == .liveActivities { - return false - } + return true } } static var generalItems: [SettingsItem] { - [.general, .gestures, .location, .notifications, .kiosk] + [.general, .gestures, .location, .notifications, .kiosk, .liveActivities] } static var integrationItems: [SettingsItem] { diff --git a/Sources/App/Settings/Settings/SettingsView.swift b/Sources/App/Settings/Settings/SettingsView.swift index 3dc3fab58..35b4b61f7 100644 --- a/Sources/App/Settings/Settings/SettingsView.swift +++ b/Sources/App/Settings/Settings/SettingsView.swift @@ -246,7 +246,7 @@ struct SettingsView: View { Label { HStack(spacing: DesignSystem.Spaces.one) { Text(item.title) - if item == .kiosk { + if [.kiosk, .liveActivities].contains(item) { LabsLabel() } }