Procházet zdrojové kódy

Categorize alarms by status

Jonas Björkert před 1 rokem
rodič
revize
4b2a6938bb

+ 16 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -189,6 +189,8 @@
 		DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; };
 		DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; };
 		DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; };
+		DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */; };
+		DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */; };
 		DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */; };
 		DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F442D479AB000884336 /* LoopOverrideView.swift */; };
 		DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */; };
@@ -561,6 +563,8 @@
 		DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
 		DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = "<group>"; };
 		DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = "<group>"; };
+		DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAlarmSheet.swift; sourceTree = "<group>"; };
+		DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmTile.swift; sourceTree = "<group>"; };
 		DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopNightscoutRemoteView.swift; sourceTree = "<group>"; };
 		DDDF6F442D479AB000884336 /* LoopOverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideView.swift; sourceTree = "<group>"; };
 		DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideViewModel.swift; sourceTree = "<group>"; };
@@ -1109,6 +1113,7 @@
 		DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = {
 			isa = PBXGroup;
 			children = (
+				DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */,
 				DDCC3A502DDC5BD4006F1C10 /* DataStructs */,
 				DDC6CA3B2DD7B9050060EE25 /* AlarmType */,
 				DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */,
@@ -1124,6 +1129,15 @@
 			path = Alarm;
 			sourceTree = "<group>";
 		};
+		DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */ = {
+			isa = PBXGroup;
+			children = (
+				DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */,
+				DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */,
+			);
+			path = AddAlarm;
+			sourceTree = "<group>";
+		};
 		DDDF6F412D479A8E00884336 /* Loop */ = {
 			isa = PBXGroup;
 			children = (
@@ -1894,12 +1908,14 @@
 				DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */,
 				DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */,
 				DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */,
+				DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */,
 				DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */,
 				DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */,
 				DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */,
 				DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */,
 				DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */,
 				FCFEECA02488157B00402A7F /* Chart.swift in Sources */,
+				DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */,
 				DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */,
 				DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */,
 				DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */,

+ 46 - 0
LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift

@@ -0,0 +1,46 @@
+// LoopFollow
+// AddAlarmSheet.swift
+// Created by Jonas Björkert.
+
+import SwiftUI
+
+struct AddAlarmSheet: View {
+    let onSelect: (AlarmType) -> Void
+    @Environment(\.dismiss) private var dismiss
+
+    private let columns = [
+        GridItem(.adaptive(minimum: 110), spacing: 16),
+    ]
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                LazyVGrid(columns: columns, spacing: 16) {
+                    ForEach(AlarmType.Group.allCases, id: \.self) { group in
+                        if AlarmType.allCases.contains(where: { $0.group == group }) {
+                            Section(header: Text(group.rawValue)
+                                .font(.headline)
+                                .frame(maxWidth: .infinity, alignment: .leading)
+                                .padding(.horizontal, 4)
+                            ) {
+                                ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in
+                                    AlarmTile(type: type) {
+                                        onSelect(type)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                .padding()
+            }
+            .navigationTitle("Add Alarm")
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("Cancel") { dismiss() }
+                }
+            }
+        }
+        .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
+    }
+}

+ 33 - 0
LoopFollow/Alarm/AddAlarm/AlarmTile.swift

@@ -0,0 +1,33 @@
+// LoopFollow
+// AlarmTile.swift
+// Created by Jonas Björkert.
+
+import SwiftUI
+
+struct AlarmTile: View {
+    let type: AlarmType
+    let action: () -> Void
+
+    var body: some View {
+        Button(action: action) {
+            VStack(spacing: 8) {
+                Image(systemName: type.icon)
+                    .font(.title2)
+                    .foregroundColor(.accentColor)
+                Text(type.rawValue)
+                    .font(.subheadline)
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+                Text(type.blurb)
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+            }
+            .padding()
+            .frame(maxWidth: .infinity, minHeight: 110)
+            .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
+        }
+        .buttonStyle(.plain)
+    }
+}

+ 90 - 104
LoopFollow/Alarm/AlarmListView.swift

@@ -4,75 +4,6 @@
 
 import SwiftUI
 
-struct AddAlarmSheet: View {
-    let onSelect: (AlarmType) -> Void
-    @Environment(\.dismiss) private var dismiss
-
-    private let columns = [
-        GridItem(.adaptive(minimum: 110), spacing: 16),
-    ]
-
-    var body: some View {
-        NavigationStack {
-            ScrollView {
-                LazyVGrid(columns: columns, spacing: 16) {
-                    ForEach(AlarmType.Group.allCases, id: \.self) { group in
-                        if AlarmType.allCases.contains(where: { $0.group == group }) {
-                            Section(header: Text(group.rawValue)
-                                .font(.headline)
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                                .padding(.horizontal, 4)
-                            ) {
-                                ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in
-                                    AlarmTile(type: type) {
-                                        onSelect(type)
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-                .padding()
-            }
-            .navigationTitle("Add Alarm")
-            .toolbar {
-                ToolbarItem(placement: .cancellationAction) {
-                    Button("Cancel") { dismiss() }
-                }
-            }
-        }
-        .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
-    }
-}
-
-private struct AlarmTile: View {
-    let type: AlarmType
-    let action: () -> Void
-
-    var body: some View {
-        Button(action: action) {
-            VStack(spacing: 8) {
-                Image(systemName: type.icon)
-                    .font(.title2)
-                    .foregroundColor(.accentColor)
-                Text(type.rawValue)
-                    .font(.subheadline)
-                    .multilineTextAlignment(.center)
-                    .lineLimit(2)
-                Text(type.blurb)
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-                    .multilineTextAlignment(.center)
-                    .lineLimit(2)
-            }
-            .padding()
-            .frame(maxWidth: .infinity, minHeight: 110)
-            .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
-        }
-        .buttonStyle(.plain)
-    }
-}
-
 private enum SheetInfo: Identifiable {
     case picker
     case editor(id: UUID, isNew: Bool)
@@ -93,39 +24,63 @@ struct AlarmListView: View {
     @State private var deleteAfterDismiss: UUID?
     @State private var selectedAlarm: Alarm?
 
-    private var sortedAlarms: [Alarm] {
-        store.value.sorted(by: Alarm.byPriorityThenSpec)
+    // MARK: - Categorized Alarms
+
+    private var snoozedAlarms: [Alarm] {
+        store.value.filter { $0.snoozedUntil ?? .distantPast > Date() && $0.isEnabled }
+            .sorted(by: Alarm.byPriorityThenSpec)
+    }
+
+    private var activeAlarms: [Alarm] {
+        store.value.filter { $0.isEnabled && ($0.snoozedUntil ?? .distantPast <= Date()) }
+            .sorted(by: Alarm.byPriorityThenSpec)
+    }
+
+    private var inactiveAlarms: [Alarm] {
+        store.value.filter { !$0.isEnabled }
+            .sorted(by: Alarm.byPriorityThenSpec)
     }
 
+    // MARK: - Formatters
+
+    private var timeFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    // MARK: - Body
+
     var body: some View {
         List {
-            ForEach(sortedAlarms) { alarm in
-                Button {
-                    selectedAlarm = alarm
-                    sheetInfo = .editor(id: alarm.id, isNew: false)
-                } label: {
-                    HStack(spacing: 12) {
-                        Glyph(
-                            symbol: alarm.type.icon,
-                            tint: alarm.isEnabled ? .white : Color(uiColor: .darkGray)
-                        )
-                        .overlay {
-                            if let until = alarm.snoozedUntil, until > Date() {
-                                Image(systemName: "zzz")
-                                    .font(.caption.bold())
-                                    .foregroundColor(.secondary)
-                                    .shadow(color: .black, radius: 2)
-                                    .offset(x: 8, y: 8)
-                            }
-                        }
+            // --- SNOOZED ALARMS SECTION ---
+            if !snoozedAlarms.isEmpty {
+                Section(header: Text("Snoozed")) {
+                    ForEach(snoozedAlarms) { alarm in
+                        alarmRow(for: alarm)
+                    }
+                }
+            }
+
+            // --- ACTIVE ALARMS SECTION ---
+            if !activeAlarms.isEmpty {
+                Section(header: Text("Active Alarms")) {
+                    ForEach(activeAlarms) { alarm in
+                        alarmRow(for: alarm)
+                    }
+                }
+            }
 
-                        Text(alarm.name)
-                            .frame(maxWidth: .infinity, alignment: .leading)
-                            .foregroundColor(.primary)
+            // --- INACTIVE ALARMS SECTION ---
+            if !inactiveAlarms.isEmpty {
+                Section(header: Text("Inactive Alarms")) {
+                    ForEach(inactiveAlarms) { alarm in
+                        alarmRow(for: alarm)
+                            .opacity(0.6)
                     }
                 }
             }
-            .onDelete(perform: deleteItems)
         }
         .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in
             sheetContent(for: info)
@@ -139,14 +94,51 @@ struct AlarmListView: View {
         .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
     }
 
-    private func deleteItems(at offsets: IndexSet) {
-        let alarmsToDelete = offsets.map { sortedAlarms[$0] }
+    // MARK: - Views
+
+    @ViewBuilder
+    private func alarmRow(for alarm: Alarm) -> some View {
+        Button {
+            selectedAlarm = alarm
+            sheetInfo = .editor(id: alarm.id, isNew: false)
+        } label: {
+            HStack(spacing: 12) {
+                Glyph(
+                    symbol: alarm.type.icon,
+                    tint: .primary
+                )
 
-        let idsToDelete = alarmsToDelete.map { $0.id }
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(alarm.name)
+                        .foregroundColor(.primary)
 
-        store.value.removeAll { idsToDelete.contains($0.id) }
+                    if let until = alarm.snoozedUntil, until > Date() {
+                        HStack(spacing: 4) {
+                            Image(systemName: "zzz")
+                                .font(.caption2)
+                            Text("Snoozed until \(until, formatter: timeFormatter)")
+                                .font(.caption)
+                        }
+                        .foregroundColor(.secondary)
+                    }
+                }
+                Spacer()
+                Image(systemName: "chevron.right")
+                    .font(.footnote.weight(.semibold))
+                    .foregroundColor(.secondary)
+            }
+        }
+        .swipeActions {
+            Button(role: .destructive) {
+                store.value.removeAll { $0.id == alarm.id }
+            } label: {
+                Label("Delete", systemImage: "trash.fill")
+            }
+        }
     }
 
+    // MARK: - Sheet Management
+
     private func handleSheetDismiss() {
         if let id = deleteAfterDismiss,
            let idx = store.value.firstIndex(where: { $0.id == id })
@@ -186,10 +178,4 @@ struct AlarmListView: View {
             }
         }
     }
-
-    private func iconOpacity(for alarm: Alarm) -> Double {
-        if !alarm.isEnabled { return 0.35 }
-        if let until = alarm.snoozedUntil, until > Date() { return 0.55 }
-        return 1.0
-    }
 }