Просмотр исходного кода

Merge branch 'add-watch-contact' of github.com:mkellerman/Trio-dev into contactTrick

polscm32 aka Marvout 1 год назад
Родитель
Сommit
897a0d690f

+ 4 - 4
FreeAPS/Sources/APS/Storage/ContactTrickStorage.swift

@@ -41,10 +41,10 @@ final class BaseContactTrickStorage: ContactTrickStorage, Injectable {
                 ContactTrickEntry(
                     name: entry.name ?? "No name provided",
                     layout: ContactTrickLayout(rawValue: entry.layout ?? "Single") ?? .single,
-                    ring: ContactTrickLargeRing(rawValue: entry.ring ?? "DontShowRing") ?? .none,
-                    primary: ContactTrickValue(rawValue: entry.primary ?? "GlucoseContactValue") ?? .glucose,
-                    top: ContactTrickValue(rawValue: entry.top ?? "NoneContactValue") ?? .none,
-                    bottom: ContactTrickValue(rawValue: entry.bottom ?? "NoneContactValue") ?? .none,
+                    ring: ContactTrickLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
+                    primary: ContactTrickValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
+                    top: ContactTrickValue(rawValue: entry.top ?? "None") ?? .none,
+                    bottom: ContactTrickValue(rawValue: entry.bottom ?? "None") ?? .none,
                     contactId: entry.contactId?.string,
                     darkMode: entry.isDarkMode,
                     ringWidth: ContactTrickEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,

+ 34 - 16
FreeAPS/Sources/Models/ContactTrickEntry.swift

@@ -1,7 +1,7 @@
 import CoreData
 import SwiftUI
 
-struct ContactTrickEntry: Hashable, Sendable {
+struct ContactTrickEntry: Hashable, Equatable, Sendable {
     var id = UUID()
     var name: String = ""
     var layout: ContactTrickLayout = .single
@@ -10,7 +10,7 @@ struct ContactTrickEntry: Hashable, Sendable {
     var top: ContactTrickValue = .none
     var bottom: ContactTrickValue = .none
     var contactId: String? = nil
-    var darkMode: Bool = true
+    var darkMode: Bool = false
     var ringWidth: RingWidth = .regular
     var ringGap: RingGap = .small
     var fontSize: FontSize = .regular
@@ -19,6 +19,24 @@ struct ContactTrickEntry: Hashable, Sendable {
     var fontWidth: Font.Width = .standard
     var managedObjectID: NSManagedObjectID?
 
+    static func == (lhs: ContactTrickEntry, rhs: ContactTrickEntry) -> Bool {
+        lhs.id == rhs.id &&
+            lhs.name == rhs.name &&
+            lhs.layout == rhs.layout &&
+            lhs.ring == rhs.ring &&
+            lhs.primary == rhs.primary &&
+            lhs.top == rhs.top &&
+            lhs.bottom == rhs.bottom &&
+            lhs.contactId == rhs.contactId &&
+            lhs.darkMode == rhs.darkMode &&
+            lhs.ringWidth == rhs.ringWidth &&
+            lhs.ringGap == rhs.ringGap &&
+            lhs.fontSize == rhs.fontSize &&
+            lhs.secondaryFontSize == rhs.secondaryFontSize &&
+            lhs.fontWeight == rhs.fontWeight &&
+            lhs.fontWidth == rhs.fontWidth
+    }
+
     // Convert `fontWeight` to a String for Core Data storage
     var fontWeightString: String {
         fontWeight.asString
@@ -112,23 +130,23 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable {
     var displayName: String {
         switch self {
         case .none:
-            return NSLocalizedString("NoneContactValue", comment: "")
+            return NSLocalizedString("None", comment: "")
         case .glucose:
-            return NSLocalizedString("GlucoseContactValue", comment: "")
+            return NSLocalizedString("Glucose Reading", comment: "")
         case .eventualBG:
-            return NSLocalizedString("EventualBGContactValue", comment: "")
+            return NSLocalizedString("Eventual Glucose", comment: "")
         case .delta:
-            return NSLocalizedString("DeltaContactValue", comment: "")
+            return NSLocalizedString("Glucose Delta", comment: "")
         case .trend:
-            return NSLocalizedString("TrendContactValue", comment: "")
+            return NSLocalizedString("Glucose Trend", comment: "")
         case .lastLoopDate:
-            return NSLocalizedString("LastLoopTimeContactValue", comment: "")
+            return NSLocalizedString("Last Loop Time", comment: "")
         case .cob:
-            return NSLocalizedString("COBContactValue", comment: "")
+            return NSLocalizedString("COB", comment: "")
         case .iob:
-            return NSLocalizedString("IOBContactValue", comment: "")
+            return NSLocalizedString("IOB", comment: "")
         case .ring:
-            return NSLocalizedString("LoopStatusContactValue", comment: "")
+            return NSLocalizedString("Loop Status", comment: "")
         }
     }
 }
@@ -159,15 +177,15 @@ enum ContactTrickLargeRing: String, JSON, CaseIterable, Identifiable, Codable {
     var displayName: String {
         switch self {
         case .none:
-            return NSLocalizedString("DontShowRing", comment: "")
+            return NSLocalizedString("Hidden", comment: "")
         case .loop:
-            return NSLocalizedString("LoopStatusRing", comment: "")
+            return NSLocalizedString("Loop Status", comment: "")
         case .iob:
-            return NSLocalizedString("IOBRing", comment: "")
+            return NSLocalizedString("Insulin on Board (IOB)", comment: "")
         case .cob:
-            return NSLocalizedString("COBRing", comment: "")
+            return NSLocalizedString("Carbs on Board (COB)", comment: "")
         case .iobcob:
-            return NSLocalizedString("IOB+COBRing", comment: "")
+            return NSLocalizedString("IOB + COB", comment: "")
         }
     }
 }

+ 4 - 0
FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift

@@ -8,6 +8,9 @@ extension ContactTrick {
         @ObservationIgnored @Injected() var contactTrickManager: ContactTrickManager!
         var contactTrickEntries = [ContactTrickEntry]()
         var units: GlucoseUnits = .mmolL
+        // Help Sheet
+        var isHelpSheetPresented: Bool = false
+        var helpSheetDetent = PresentationDetent.large
 
         var previewState: ContactTrickState {
             ContactTrickState(
@@ -93,6 +96,7 @@ extension ContactTrick {
             // 3. Update the entry with the `contactId`.
             var updatedEntry = entry
             updatedEntry.contactId = contactId
+            updatedEntry.name = name
 
             // 4. Save the contact to Core Data.
             await addContactTrickEntry(updatedEntry)

+ 143 - 76
FreeAPS/Sources/Modules/ContactTrick/View/AddContactTrickSheet.swift

@@ -2,16 +2,18 @@ import SwiftUI
 
 struct AddContactTrickSheet: View {
     @Environment(\.dismiss) var dismiss
-    var state: ContactTrick.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    @ObservedObject var state: ContactTrick.StateModel
 
-    @State private var name: String = ""
     @State private var isDarkMode: Bool = false
     @State private var ringWidth: ContactTrickEntry.RingWidth = .regular
     @State private var ringGap: ContactTrickEntry.RingGap = .small
     @State private var layout: ContactTrickLayout = .single
     @State private var primary: ContactTrickValue = .glucose
     @State private var top: ContactTrickValue = .none
-    @State private var bottom: ContactTrickValue = .none
+    @State private var bottom: ContactTrickValue = .trend
     @State private var ring: ContactTrickLargeRing = .none
     @State private var fontSize: ContactTrickEntry.FontSize = .regular
     @State private var secondaryFontSize: ContactTrickEntry.FontSize = .small
@@ -21,7 +23,7 @@ struct AddContactTrickSheet: View {
     private var previewEntry: ContactTrickEntry {
         ContactTrickEntry(
             id: UUID(),
-            name: name,
+            name: "", // automatically set and populated
             layout: layout,
             ring: ring,
             primary: primary,
@@ -40,96 +42,161 @@ struct AddContactTrickSheet: View {
 
     var body: some View {
         NavigationView {
-            Form {
-                // TODO: - make this beautiful @Dan
-
+            VStack {
                 // Preview Section
-                Section {
-                    HStack {
-                        Spacer()
+                HStack {
+                    Spacer()
+                    ZStack {
+                        Circle()
+                            .fill(previewEntry.darkMode ? .black : .white)
+                            .foregroundColor(.white)
+                            .frame(width: 100, height: 100)
                         Image(uiImage: ContactPicture.getImage(contact: previewEntry, state: state.previewState))
                             .resizable()
-                            .aspectRatio(1, contentMode: .fit)
                             .frame(width: 100, height: 100)
                             .clipShape(Circle())
-                            .overlay(Circle().stroke(Color.gray, lineWidth: 1))
-                        Spacer()
+                        Circle()
+                            .stroke(lineWidth: 2)
+                            .foregroundColor(.white)
+                            .frame(width: 100, height: 100)
                     }
+                    Spacer()
                 }
-
-                // Name Section
-                TextField("Name", text: $name)
-
-                // Layout Section
-                Section(header: Text("Layout")) {
-                    Toggle("Dark Mode", isOn: $isDarkMode)
-                    Picker("Layout", selection: $layout) {
-                        ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
-                            Text(layout.displayName).tag(layout)
+                .padding(.top, 40)
+                .padding(.bottom)
+
+                Form {
+                    // Layout Section
+                    Section(header: Text("Style")) {
+                        Picker("Layout", selection: $layout) {
+                            ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
+                                Text(layout.displayName).tag(layout)
+                            }
                         }
-                    }
-                    .pickerStyle(SegmentedPickerStyle())
-                }
-
-                // Primary Value Section
-                Section(header: Text("Primary Value")) {
-                    Picker("Primary", selection: $primary) {
-                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                            Text(value.displayName).tag(value)
+                        Toggle("Dark Mode", isOn: $isDarkMode)
+                    }.listRowBackground(Color.chart)
+
+                    // Primary Value Section
+                    Section(header: Text("Display Values")) {
+                        if layout == .single {
+                            Picker("Primary", selection: $primary) {
+                                ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                    Text(value.displayName).tag(value)
+                                }
+                            }
                         }
-                    }
-                }
 
-                // Additional Values Section
-                Section(header: Text("Additional Values")) {
-                    Picker("Top Value", selection: $top) {
-                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                            Text(value.displayName).tag(value)
+                        Picker("Top Value", selection: $top) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
                         }
-                    }
-                    Picker("Bottom Value", selection: $bottom) {
-                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                            Text(value.displayName).tag(value)
+                        Picker("Bottom Value", selection: $bottom) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
                         }
-                    }
-                }
 
-                // Ring Settings Section
-                Section(header: Text("Ring Settings")) {
-                    Picker("Ring Type", selection: $ring) {
-                        ForEach(ContactTrickLargeRing.allCases, id: \.self) { ring in
-                            Text(ring.displayName).tag(ring)
+                    }.listRowBackground(Color.chart)
+
+                    // Ring Settings Section
+                    Section(header: Text("Ring Settings")) {
+                        Picker("Ring Type", selection: $ring) {
+                            ForEach(ContactTrickLargeRing.allCases, id: \.self) { ring in
+                                Text(ring.displayName).tag(ring)
+                            }
                         }
-                    }
-                    Picker("Ring Width", selection: $ringWidth) {
-                        ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
-                            Text(width.displayName).tag(width)
+
+                        if ring != .none {
+                            Picker("Ring Width", selection: $ringWidth) {
+                                ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
+                                    Text(width.displayName).tag(width)
+                                }
+                            }
+                            Picker("Ring Gap", selection: $ringGap) {
+                                ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
+                                    Text(gap.displayName).tag(gap)
+                                }
+                            }
                         }
+                    }.listRowBackground(Color.chart)
+
+                    // Font Settings Section
+                    Section(header: Text("Font Settings")) {
+                        fontSizePicker
+                        secondaryFontSizePicker
+                        fontWeightPicker
+                        fontWidthPicker
+                    }.listRowBackground(Color.chart)
+                }
+
+                stickySaveButton
+            }
+            .navigationTitle("Add Contact Items")
+            .navigationBarTitleDisplayMode(.inline)
+            .listSectionSpacing(10)
+            .padding(.top, 30)
+            .ignoresSafeArea(edges: .top)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Cancel") {
+                        dismiss()
                     }
-                    Picker("Ring Gap", selection: $ringGap) {
-                        ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
-                            Text(gap.displayName).tag(gap)
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(
+                        action: {
+                            state.isHelpSheetPresented.toggle()
+                        },
+                        label: {
+                            Image(systemName: "questionmark.circle")
                         }
-                    }
+                    )
                 }
+            }
+            .sheet(isPresented: $state.isHelpSheetPresented) {
+                NavigationStack {
+                    List {
+                        Text("Lorem Ipsum Dolor Sit Amet")
+                    }
+                    .padding(.trailing, 10)
+                    .navigationBarTitle("Help", displayMode: .inline)
 
-                // Font Settings Section
-                Section(header: Text("Font Settings")) {
-                    fontSizePicker
-                    secondaryFontSizePicker
-                    fontWeightPicker
-                    fontWidthPicker
+                    Button { state.isHelpSheetPresented.toggle() }
+                    label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                        .buttonStyle(.bordered)
+                        .padding(.top)
                 }
+                .padding()
+                .presentationDetents(
+                    [.fraction(0.9), .large],
+                    selection: $state.helpSheetDetent
+                )
             }
-            .navigationBarTitle("Add Contact Trick", displayMode: .inline)
-            .navigationBarItems(
-                leading: Button("Cancel") {
-                    dismiss()
-                },
-                trailing: Button("Save") {
-                    saveNewEntry()
-                }
-            )
+        }
+    }
+
+    var stickySaveButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                saveNewEntry()
+            }, label: {
+                Text("Save").padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                .background(Color(.systemBlue))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
         }
     }
 
@@ -155,7 +222,7 @@ struct AddContactTrickSheet: View {
                 [Font.Weight.light, Font.Weight.regular, Font.Weight.medium, Font.Weight.bold, Font.Weight.black],
                 id: \.self
             ) { weight in
-                Text("\(weight)".capitalized).tag(weight)
+                Text("\(weight.displayName)".capitalized).tag(weight)
             }
         }
     }
@@ -166,7 +233,7 @@ struct AddContactTrickSheet: View {
                 [Font.Width.standard, Font.Width.condensed, Font.Width.expanded],
                 id: \.self
             ) { width in
-                Text("\(width)".capitalized).tag(width)
+                Text("\(width.displayName)".capitalized).tag(width)
             }
         }
     }
@@ -174,7 +241,7 @@ struct AddContactTrickSheet: View {
     private func saveNewEntry() {
         // Save the currently previewed entry
         Task {
-            await state.createAndSaveContactTrick(entry: previewEntry, name: name)
+            await state.createAndSaveContactTrick(entry: previewEntry, name: "Trio \(state.contactTrickEntries.count + 1)")
             dismiss()
         }
     }

+ 172 - 48
FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickDetailView.swift

@@ -2,85 +2,146 @@ import SwiftUI
 
 struct ContactTrickDetailView: View {
     @Environment(\.dismiss) var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
     @ObservedObject var state: ContactTrick.StateModel
 
     @State private var contactTrickEntry: ContactTrickEntry
+    @State private var initialContactTrickEntry: ContactTrickEntry
 
     init(entry: ContactTrickEntry, state: ContactTrick.StateModel) {
         self.state = state
         _contactTrickEntry = State(initialValue: entry)
+        _initialContactTrickEntry = State(initialValue: entry)
     }
 
     var body: some View {
-        Form {
-            Section {
-                HStack {
-                    // TODO: - make this beautiful @Dan
-                    Spacer()
+        VStack {
+            HStack {
+                // TODO: - make this beautiful @Dan
+                Spacer()
+                ZStack {
+                    Circle()
+                        .fill(contactTrickEntry.darkMode ? .black : .white)
+                        .foregroundColor(.white)
+                        .frame(width: 100, height: 100)
                     Image(uiImage: ContactPicture.getImage(contact: contactTrickEntry, state: state.previewState))
                         .resizable()
                         .frame(width: 100, height: 100)
                         .clipShape(Circle())
-                        .overlay(Circle().stroke(Color.gray, lineWidth: 1))
-                    Spacer()
+                    Circle()
+                        .stroke(lineWidth: 2)
+                        .foregroundColor(.white)
+                        .frame(width: 100, height: 100)
                 }
+                Spacer()
             }
+            .padding(.top, 80)
+            .padding(.bottom)
 
-            TextField("Name", text: $contactTrickEntry.name)
-            Section(header: Text("Layout")) {
-                Toggle("Dark Mode", isOn: $contactTrickEntry.darkMode)
-                Picker("Layout", selection: $contactTrickEntry.layout) {
-                    ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
-                        Text(layout.displayName).tag(layout)
+            Form {
+                Section(header: Text("Style")) {
+                    Picker("Layout", selection: $contactTrickEntry.layout) {
+                        ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
+                            Text(layout.displayName).tag(layout)
+                        }
                     }
-                }
-                .pickerStyle(SegmentedPickerStyle())
-            }
+                    Toggle("Dark Mode", isOn: $contactTrickEntry.darkMode)
+                }.listRowBackground(Color.chart)
 
-            Section(header: Text("Primary Value")) {
-                Picker("Primary", selection: $contactTrickEntry.primary) {
-                    ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                        Text(value.displayName).tag(value)
+                Section(header: Text("Display Values")) {
+                    if contactTrickEntry.layout == .single {
+                        Picker("Primary", selection: $contactTrickEntry.primary) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
+                        }
                     }
-                }
-            }
 
-            Section(header: Text("Additional Values")) {
-                Picker("Top Value", selection: $contactTrickEntry.top) {
-                    ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                        Text(value.displayName).tag(value)
+                    Picker("Top Value", selection: $contactTrickEntry.top) {
+                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                            Text(value.displayName).tag(value)
+                        }
                     }
-                }
 
-                Picker("Bottom Value", selection: $contactTrickEntry.bottom) {
-                    ForEach(ContactTrickValue.allCases, id: \.id) { value in
-                        Text(value.displayName).tag(value)
+                    Picker("Bottom Value", selection: $contactTrickEntry.bottom) {
+                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                            Text(value.displayName).tag(value)
+                        }
                     }
-                }
-            }
+                }.listRowBackground(Color.chart)
 
-            Section(header: Text("Ring Settings")) {
-                Picker("Ring Width", selection: $contactTrickEntry.ringWidth) {
-                    ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
-                        Text(width.displayName)
-                            .tag(width)
+                // Ring Settings Section
+                Section(header: Text("Ring Settings")) {
+                    Picker("Ring Type", selection: $contactTrickEntry.ring) {
+                        ForEach(ContactTrickLargeRing.allCases, id: \.self) { ring in
+                            Text(ring.displayName).tag(ring)
+                        }
                     }
-                }
 
-                Picker("Ring Gap", selection: $contactTrickEntry.ringGap) {
-                    ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
-                        Text(gap.displayName)
-                            .tag(gap)
+                    if contactTrickEntry.ring != .none {
+                        Picker("Ring Width", selection: $contactTrickEntry.ringWidth) {
+                            ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
+                                Text(width.displayName).tag(width)
+                            }
+                        }
+                        Picker("Ring Gap", selection: $contactTrickEntry.ringGap) {
+                            ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
+                                Text(gap.displayName).tag(gap)
+                            }
+                        }
                     }
-                }
+                }.listRowBackground(Color.chart)
+
+                // Font Settings Section
+                Section(header: Text("Font Settings")) {
+                    fontSizePicker
+                    secondaryFontSizePicker
+                    fontWeightPicker
+                    fontWidthPicker
+                }.listRowBackground(Color.chart)
             }
         }
-        .navigationBarTitle("Edit Contact Trick", displayMode: .inline)
-        .navigationBarItems(
-            trailing: Button("Save") {
-                saveChanges()
+        .navigationTitle("Edit Contact Items")
+        .navigationBarTitleDisplayMode(.inline)
+        .safeAreaInset(edge: .bottom, spacing: 0) { stickySaveButton }
+        .listSectionSpacing(10)
+        .padding(.top, 30)
+        .ignoresSafeArea(edges: .top)
+        .scrollContentBackground(.hidden)
+        .background(appState.trioBackgroundColor(for: colorScheme))
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Button(
+                    action: {
+                        state.isHelpSheetPresented.toggle()
+                    },
+                    label: {
+                        Image(systemName: "questionmark.circle")
+                    }
+                )
+            }
+        }
+        .sheet(isPresented: $state.isHelpSheetPresented) {
+            NavigationStack {
+                List {
+                    Text("Lorem Ipsum Dolor Sit Amet")
+                }
+                .padding(.trailing, 10)
+                .navigationBarTitle("Help", displayMode: .inline)
+
+                Button { state.isHelpSheetPresented.toggle() }
+                label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                    .buttonStyle(.bordered)
+                    .padding(.top)
             }
-        )
+            .padding()
+            .presentationDetents(
+                [.fraction(0.9), .large],
+                selection: $state.helpSheetDetent
+            )
+        }
     }
 
     private func saveChanges() {
@@ -89,4 +150,67 @@ struct ContactTrickDetailView: View {
             dismiss()
         }
     }
+
+    var stickySaveButton: some View {
+        var isUnchanged: Bool { initialContactTrickEntry == contactTrickEntry }
+
+        return ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                saveChanges()
+            }, label: {
+                Text("Save").padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                .background(isUnchanged ? Color(.systemGray4) : Color(.systemBlue))
+                .disabled(isUnchanged)
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
+        }
+    }
+
+    private var fontSizePicker: some View {
+        Picker("Font Size", selection: $contactTrickEntry.fontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var secondaryFontSizePicker: some View {
+        Picker("Secondary Font Size", selection: $contactTrickEntry.secondaryFontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var fontWeightPicker: some View {
+        Picker("Font Weight", selection: $contactTrickEntry.fontWeight) {
+            ForEach(
+                [Font.Weight.light, Font.Weight.regular, Font.Weight.medium, Font.Weight.bold, Font.Weight.black],
+                id: \.self
+            ) { weight in
+                Text("\(weight.displayName)".capitalized).tag(weight)
+            }
+        }
+    }
+
+    private var fontWidthPicker: some View {
+        Picker("Font Width", selection: $contactTrickEntry.fontWidth) {
+            ForEach(
+                [Font.Width.standard, Font.Width.condensed, Font.Width.expanded],
+                id: \.self
+            ) { width in
+                Text("\(width.displayName)".capitalized).tag(width)
+            }
+        }
+    }
 }

+ 53 - 33
FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift

@@ -7,56 +7,76 @@ extension ContactTrick {
     struct RootView: BaseView {
         let resolver: Resolver
         @State var state = StateModel()
-        @State private var isAddSheetPresented = false
+        @State private var isAddSheetPresented: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
 
         var body: some View {
-            NavigationView {
+            Form {
                 contactTrickList
-                    .navigationTitle("Contact Tricks")
-                    .onAppear(perform: configureView)
-                    .navigationBarItems(
-                        trailing: Button(action: {
-                            isAddSheetPresented.toggle()
-                        }) {
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Contacts Configuration")
+            .navigationBarTitleDisplayMode(.large)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .toolbar {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(action: {
+                        isAddSheetPresented.toggle()
+                    }) {
+                        HStack {
+                            Text("Add Contact")
                             Image(systemName: "plus")
                         }
-                    )
-                    .sheet(isPresented: $isAddSheetPresented) {
-                        AddContactTrickSheet(state: state)
                     }
+                }
+            }
+            .sheet(isPresented: $isAddSheetPresented) {
+                AddContactTrickSheet(state: state)
             }
         }
 
         private var contactTrickList: some View {
             List {
-                ForEach(state.contactTrickEntries, id: \.id) { entry in
-                    NavigationLink(destination: ContactTrickDetailView(entry: entry, state: state)) {
-                        HStack {
-                            // TODO: - make this beautiful @Dan
-                            ZStack {
-                                Circle()
-                                    .fill(entry.darkMode ? .black : .white)
-                                    .foregroundColor(.white)
-                                    .frame(width: 40, height: 40)
+                if state.contactTrickEntries.isEmpty {
+                    Section(
+                        header: Text(""),
+                        content: {
+                            Text("No Contact Trick Entries.")
+                        }
+                    ).listRowBackground(Color.chart)
+                } else {
+                    ForEach(state.contactTrickEntries, id: \.id) { entry in
+                        NavigationLink(destination: ContactTrickDetailView(entry: entry, state: state)) {
+                            HStack {
+                                // TODO: - make this beautiful @Dan
+                                ZStack {
+                                    Circle()
+                                        .fill(entry.darkMode ? .black : .white)
+                                        .foregroundColor(.white)
+                                        .frame(width: 40, height: 40)
 
-                                Image(uiImage: ContactPicture.getImage(contact: entry, state: state.previewState))
-                                    .resizable()
-                                    .frame(width: 40, height: 40)
-                                    .clipShape(Circle())
+                                    Image(uiImage: ContactPicture.getImage(contact: entry, state: state.previewState))
+                                        .resizable()
+                                        .frame(width: 40, height: 40)
+                                        .clipShape(Circle())
 
-                                Circle()
-                                    .stroke(lineWidth: 2)
-                                    .foregroundColor(.white)
-                                    .frame(width: 40, height: 40)
-                            }
+                                    Circle()
+                                        .stroke(lineWidth: 2)
+                                        .foregroundColor(.white)
+                                        .frame(width: 40, height: 40)
+                                }
 
-                            // Entry name
-                            Text("\(entry.name)")
+                                // Entry name
+                                Text("\(entry.name)")
+                            }
                         }
                     }
+                    .onDelete(perform: onDelete)
                 }
-                .onDelete(perform: onDelete)
-            }
+            }.listRowBackground(Color.chart)
         }
 
         private func onDelete(offsets: IndexSet) {

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -574,7 +574,7 @@ extension Home {
                                     Color.insulin.opacity(0.1)
                             ) : Color.clear // Use clear and add the Material in the background
                     )
-                    .background(.ultraThinMaterial)
+                    .background(.ultraThinMaterial.opacity(colorScheme == .dark ? 0.35 : 0))
                     .clipShape(RoundedRectangle(cornerRadius: 15))
                     .frame(height: geo.size.height * 0.08)
                     .shadow(

+ 4 - 3
FreeAPS/Sources/Modules/Settings/View/Subviews/NotificationsView.swift

@@ -31,7 +31,8 @@ struct NotificationsView: BaseView {
                 content: {
                     manageNotifications
                 }
-            )
+            ).listRowBackground(Color.chart)
+
             Section {
                 VStack {
                     notificationsEnabledStatus
@@ -59,6 +60,7 @@ struct NotificationsView: BaseView {
                     }.padding(.top)
                 }.padding(.bottom)
             }.listRowBackground(Color.chart)
+
             Section(
                 header: Text("Notification Center"),
                 content: {
@@ -71,8 +73,7 @@ struct NotificationsView: BaseView {
 
                     Text("Calendar Events").navigationLink(to: .calendarEventSettings, from: self)
                 }
-            )
-            .listRowBackground(Color.chart)
+            ).listRowBackground(Color.chart)
         }
         .onReceive(
             resolver.resolve(AlertPermissionsChecker.self)!.$notificationsDisabled,

+ 2 - 2
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift

@@ -97,11 +97,11 @@ struct WatchConfigAppleWatchView: BaseView {
             )
 
             Section(
-                header: Text("Complications"),
+                header: Text("Contact Trick"),
                 content: {
                     VStack {
                         HStack {
-                            NavigationLink("Contact image") {
+                            NavigationLink("Contacts Configuration") {
                                 ContactTrick.RootView(resolver: resolver)
                             }.foregroundStyle(Color.accentColor)
                         }

+ 62 - 0
FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift

@@ -17,6 +17,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var contactTrickStorage: ContactTrickStorage!
     @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var fileStorage: FileStorage!
 
     private let contactStore = CNContactStore()
 
@@ -128,6 +129,55 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
         }
     }
 
+    private func getCurrentGlucoseTarget() async -> Decimal? {
+        let now = Date()
+        let calendar = Calendar.current
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "HH:mm"
+        dateFormatter.timeZone = TimeZone.current
+
+        let bgTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+            ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+            ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+        let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
+
+        for (index, entry) in entries.enumerated() {
+            guard let entryTime = dateFormatter.date(from: entry.start) else {
+                print("Invalid entry start time: \(entry.start)")
+                continue
+            }
+
+            let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+            let entryStartTime = calendar.date(
+                bySettingHour: entryComponents.hour!,
+                minute: entryComponents.minute!,
+                second: entryComponents.second!,
+                of: now
+            )!
+
+            let entryEndTime: Date
+            if index < entries.count - 1,
+               let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
+            {
+                let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                entryEndTime = calendar.date(
+                    bySettingHour: nextEntryComponents.hour!,
+                    minute: nextEntryComponents.minute!,
+                    second: nextEntryComponents.second!,
+                    of: now
+                )!
+            } else {
+                entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+            }
+
+            if now >= entryStartTime, now < entryEndTime {
+                return entry.value
+            }
+        }
+
+        return nil
+    }
+
     // MARK: - Configure ContactTrickState in order to update ContactTrickImage
 
     /// Updates the `ContactTrickState` with the latest data from Core Data.
@@ -177,6 +227,18 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
             let eventualBGAsString = Formatter.decimalFormatterWithOneFractionDigit.string(from: eventualBG)
             state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
         }
+
+        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+        let hardCodedLow = Decimal(55)
+        let hardCodedHigh = Decimal(220)
+        let isDynamicColorScheme = settingsManager.settings.glucoseColorScheme == .dynamicColor
+
+        state.highGlucoseColorValue = isDynamicColorScheme ? hardCodedHigh : settingsManager.settings.highGlucose
+        state.lowGlucoseColorValue = isDynamicColorScheme ? hardCodedLow : settingsManager.settings.lowGlucose
+        state
+            .targetGlucose = await getCurrentGlucoseTarget() ??
+            (settingsManager.settings.units == .mgdL ? Decimal(100) : 100.asMmolL)
+        state.glucoseColorScheme = settingsManager.settings.glucoseColorScheme
     }
 
     // MARK: - Interactions with CNContactStore API

+ 16 - 2
FreeAPS/Sources/Services/ContactTrick/ContactTrickPicture.swift

@@ -261,9 +261,23 @@ struct ContactPicture: View {
         default: nil
         }
 
+        let glucoseValue = Decimal(string: state.glucose ?? "100") ?? 100
+
+        let dynamicColor: Color = FreeAPS.getDynamicGlucoseColor(
+            glucoseValue: glucoseValue,
+            highGlucoseColorValue: state.highGlucoseColorValue,
+            lowGlucoseColorValue: state.lowGlucoseColorValue,
+            targetGlucose: state.targetGlucose,
+            glucoseColorScheme: state.glucoseColorScheme
+        )
+
         let textColor: Color = switch value {
-        case .cob: .loopYellow
-        default: color
+        case .cob:
+            .loopYellow
+        case .glucose:
+            dynamicColor
+        default:
+            color
         }
 
         if let text = text {

+ 4 - 0
FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift

@@ -12,4 +12,8 @@ struct ContactTrickState: Codable {
     var eventualBG: String?
     var maxIOB: Decimal = 10.0
     var maxCOB: Decimal = 120.0
+    var highGlucoseColorValue: Decimal = 180.0
+    var lowGlucoseColorValue: Decimal = 70.0
+    var glucoseColorScheme: GlucoseColorScheme = .staticColor
+    var targetGlucose: Decimal = 100.0
 }