Procházet zdrojové kódy

Merge branch 'dynamic-island-graph' into feature

polscm32 před 2 roky
rodič
revize
a6d56528bc

+ 6 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -315,6 +315,8 @@
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
+		BD188BEC2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
+		BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
@@ -875,6 +877,7 @@
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
+		BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBobble.swift; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
@@ -2061,6 +2064,7 @@
 			children = (
 			children = (
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
+				BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
 			);
 			);
@@ -2902,6 +2906,7 @@
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
+				BD188BEC2B1B805B00B183BF /* WidgetBobble.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
@@ -3059,6 +3064,7 @@
 				6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
+				BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};

+ 3 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift

@@ -7,6 +7,9 @@ struct LiveActivityAttributes: ActivityAttributes {
         let trendSystemImage: String?
         let trendSystemImage: String?
         let change: String
         let change: String
         let date: Date
         let date: Date
+        let chart: [Int16]
+        let chartDate: [Date?]
+        let rotationDegrees: Double
     }
     }
 
 
     let startDate: Date
     let startDate: Date

+ 29 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -21,7 +21,7 @@ extension LiveActivityAttributes.ContentState {
             .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
             .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
     }
     }
 
 
-    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool) {
+    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool, chart: [Readings]) {
         guard let glucose = bg.glucose,
         guard let glucose = bg.glucose,
               bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
               bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
         else {
         else {
@@ -31,38 +31,56 @@ extension LiveActivityAttributes.ContentState {
         let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
         let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
 
 
         let trendString: String?
         let trendString: String?
+        var rotationDegrees: Double = 0.0
         switch bg.direction {
         switch bg.direction {
         case .doubleUp,
         case .doubleUp,
              .singleUp,
              .singleUp,
              .tripleUp:
              .tripleUp:
             trendString = "arrow.up"
             trendString = "arrow.up"
+            rotationDegrees = -90
 
 
         case .fortyFiveUp:
         case .fortyFiveUp:
             trendString = "arrow.up.right"
             trendString = "arrow.up.right"
+            rotationDegrees = -45
 
 
         case .flat:
         case .flat:
             trendString = "arrow.right"
             trendString = "arrow.right"
+            rotationDegrees = 0
 
 
         case .fortyFiveDown:
         case .fortyFiveDown:
             trendString = "arrow.down.right"
             trendString = "arrow.down.right"
+            rotationDegrees = 45
 
 
         case .doubleDown,
         case .doubleDown,
              .singleDown,
              .singleDown,
              .tripleDown:
              .tripleDown:
             trendString = "arrow.down"
             trendString = "arrow.down"
+            rotationDegrees = 90
 
 
         case .notComputable,
         case .notComputable,
              Optional.none,
              Optional.none,
              .rateOutOfRange,
              .rateOutOfRange,
              .some(.none):
              .some(.none):
             trendString = nil
             trendString = nil
+            rotationDegrees = 0
         }
         }
 
 
         let change = prev?.glucose.map({
         let change = prev?.glucose.map({
             Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
             Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
         }) ?? ""
         }) ?? ""
 
 
-        self.init(bg: formattedBG, trendSystemImage: trendString, change: change, date: bg.dateString)
+        let chartBG = chart.map(\.glucose)
+
+        let chartDate = chart.map(\.date)
+
+        self.init(
+            bg: formattedBG,
+            trendSystemImage: trendString,
+            change: change,
+            date: bg.dateString,
+            chart: chartBG,
+            chartDate: chartDate, rotationDegrees: rotationDegrees
+        )
     }
     }
 }
 }
 
 
@@ -194,10 +212,18 @@ extension LiveActivityBridge: GlucoseObserver {
             self.latestGlucose = glucose.last
             self.latestGlucose = glucose.last
         }
         }
 
 
+//        let last72Glucose = Array(glucose.dropLast().suffix(72))
+        let coreDataStorage = CoreDataStorage()
+
+//        let fetchGlucose = coreDataStorage.fetchGlucose(interval: DateFilter().day)
+        let sixHoursAgo = Calendar.current.date(byAdding: .hour, value: -6, to: Date()) ?? Date()
+        let fetchGlucose = coreDataStorage.fetchGlucose(interval: sixHoursAgo as NSDate)
+
         guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
         guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
             new: bg,
             new: bg,
             prev: latestGlucose,
             prev: latestGlucose,
-            mmol: settings.units == .mmolL
+            mmol: settings.units == .mmolL,
+            chart: fetchGlucose
         ) else {
         ) else {
             // no bg or value stale. Don't update the activity if there already is one, just let it turn stale so that it can still be used once current bg is available again
             // no bg or value stale. Don't update the activity if there already is one, just let it turn stale so that it can still be used once current bg is available again
             return
             return

+ 92 - 29
LiveActivity/LiveActivity.swift

@@ -1,4 +1,5 @@
 import ActivityKit
 import ActivityKit
+import Charts
 import SwiftUI
 import SwiftUI
 import WidgetKit
 import WidgetKit
 
 
@@ -19,46 +20,107 @@ struct LiveActivity: Widget {
     }
     }
 
 
     func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
     func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
-        Text("Updated: \(dateFormatter.string(from: context.state.date))")
+        Text(dateFormatter.string(from: context.state.date))
     }
     }
 
 
     func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
     func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
         if context.isStale {
         if context.isStale {
             Text("--")
             Text("--")
         } else {
         } else {
-            Text(context.state.bg)
+            Text(context.state.bg).fontWeight(.bold)
         }
         }
     }
     }
 
 
-    @ViewBuilder func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+    @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
         if context.isStale {
         if context.isStale {
             Text("--")
             Text("--")
         } else {
         } else {
-            Text(context.state.bg)
             if let trendSystemImage = context.state.trendSystemImage {
             if let trendSystemImage = context.state.trendSystemImage {
                 Image(systemName: trendSystemImage)
                 Image(systemName: trendSystemImage)
             }
             }
         }
         }
     }
     }
 
 
+    @ViewBuilder func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("--")
+        } else {
+            HStack {
+                Text(context.state.bg).fontWeight(.bold)
+                if let trendSystemImage = context.state.trendSystemImage {
+                    Image(systemName: trendSystemImage)
+                }
+            }
+        }
+    }
+
+    @ViewBuilder func bobble(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        @State var angularGradient = AngularGradient(colors: [
+            Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
+            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
+            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
+            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
+            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902),
+            Color(red: 0.7215686275, green: 0.3411764706, blue: 1)
+        ], center: .center, startAngle: .degrees(270), endAngle: .degrees(-90))
+        let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
+
+        WidgetBobble(gradient: angularGradient, color: triangleColor)
+            .rotationEffect(.degrees(context.state.rotationDegrees))
+    }
+
+    @ViewBuilder func chart(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("No data available")
+        } else {
+            Chart {
+                ForEach(context.state.chart.indices, id: \.self) { index in
+                    LineMark(
+                        x: .value("Time", context.state.chartDate[index] ?? Date()),
+                        y: .value("Value", context.state.chart[index] ?? 0)
+                    ).foregroundStyle(Color.green.gradient).symbolSize(12)
+                }
+            }.chartPlotStyle { plotContent in
+                plotContent.background(.cyan.opacity(0.1))
+            }
+            .chartYAxis {
+                AxisMarks(position: .leading)
+            }
+            .chartXAxis {
+                AxisMarks(position: .automatic)
+            }
+        }
+    }
+
     var body: some WidgetConfiguration {
     var body: some WidgetConfiguration {
         ActivityConfiguration(for: LiveActivityAttributes.self) { context in
         ActivityConfiguration(for: LiveActivityAttributes.self) { context in
             // Lock screen/banner UI goes here
             // Lock screen/banner UI goes here
 
 
-            HStack(spacing: 3) {
-                bgAndTrend(context: context).font(.title)
-                Spacer()
-                VStack(alignment: .trailing, spacing: 5) {
-                    changeLabel(context: context).font(.title3)
-                    updatedLabel(context: context).font(.caption).foregroundStyle(.black.opacity(0.7))
+            HStack(spacing: 2) {
+                VStack {
+                    chart(context: context).frame(width: UIScreen.main.bounds.width / 1.7)
+                }.padding(.vertical, 5).padding(.horizontal, 15)
+                Divider()
+                VStack {
+                    ZStack {
+                        bobble(context: context)
+                            .scaleEffect(0.6)
+                            .clipped()
+                        VStack {
+//                            bgAndTrend(context: context).imageScale(.small).font(.title2)
+                            bgLabel(context: context).font(.title2).imageScale(.small)
+                            changeLabel(context: context).font(.callout)
+                        }
+                    }.padding(.trailing, 5).padding(.top, 5)
+                    updatedLabel(context: context).font(.caption).padding(.bottom)
                 }
                 }
             }
             }
             .privacySensitive()
             .privacySensitive()
             .imageScale(.small)
             .imageScale(.small)
             .padding(.all, 15)
             .padding(.all, 15)
             .background(Color.white.opacity(0.2))
             .background(Color.white.opacity(0.2))
-            .foregroundColor(Color.black)
-            .activityBackgroundTint(Color.cyan.opacity(0.2))
+            .foregroundColor(Color.white)
+            .activityBackgroundTint(Color.black.opacity(0.7))
             .activitySystemActionForegroundColor(Color.black)
             .activitySystemActionForegroundColor(Color.black)
 
 
         } dynamicIsland: { context in
         } dynamicIsland: { context in
@@ -76,6 +138,7 @@ struct LiveActivity: Widget {
                 DynamicIslandExpandedRegion(.bottom) {
                 DynamicIslandExpandedRegion(.bottom) {
                     updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
                     updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
                         .padding(.bottom, 5)
                         .padding(.bottom, 5)
+                    chart(context: context).frame(height: 70)
                 }
                 }
             } compactLeading: {
             } compactLeading: {
                 HStack(spacing: 1) {
                 HStack(spacing: 1) {
@@ -92,20 +155,20 @@ struct LiveActivity: Widget {
     }
     }
 }
 }
 
 
-private extension LiveActivityAttributes {
-    static var preview: LiveActivityAttributes {
-        LiveActivityAttributes(startDate: Date())
-    }
-}
-
-private extension LiveActivityAttributes.ContentState {
-    static var test: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: "+2", date: Date())
-    }
-}
-
-#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
-    LiveActivity()
-} contentStates: {
-    LiveActivityAttributes.ContentState.test
-}
+// private extension LiveActivityAttributes {
+//    static var preview: LiveActivityAttributes {
+//        LiveActivityAttributes(startDate: Date())
+//    }
+// }
+//
+// private extension LiveActivityAttributes.ContentState {
+//    static var test: LiveActivityAttributes.ContentState {
+//        LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: "+2", date: Date())
+//    }
+// }
+//
+// #Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
+//    LiveActivity()
+// } contentStates: {
+//    LiveActivityAttributes.ContentState.test
+// }

+ 66 - 0
LiveActivity/WidgetBobble.swift

@@ -0,0 +1,66 @@
+import SwiftUI
+
+struct WidgetBobble: View {
+    @Environment(\.colorScheme) var colorScheme
+
+    let gradient: AngularGradient
+    let color: Color
+
+    var body: some View {
+        HStack(alignment: .center) {
+            ZStack {
+                Group {
+                    CircleShape(gradient: gradient)
+                    TriangleShape(color: color)
+                }
+                CircleShape(gradient: gradient)
+            }
+        }
+    }
+}
+
+struct CircleShape: View {
+    @Environment(\.colorScheme) var colorScheme
+
+    let gradient: AngularGradient
+
+    var body: some View {
+//        let colorBackground: Color = colorScheme == .dark ? Color(
+//            red: 0.05490196078,
+//            green: 0.05490196078,
+//            blue: 0.05490196078
+//        ) : .white
+
+        Circle()
+            .stroke(gradient, lineWidth: 10)
+            .background(Circle().fill(.clear))
+            .frame(width: 130, height: 130)
+    }
+}
+
+struct TriangleShape: View {
+    let color: Color
+
+    var body: some View {
+        Triangle()
+            .fill(color)
+            .frame(width: 35, height: 35)
+            .rotationEffect(.degrees(90))
+            .offset(x: 78)
+    }
+}
+
+struct Triangle: Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+
+        let cornerRadius: CGFloat = 2
+
+        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.midX, y: rect.maxY))
+        path.closeSubpath()
+
+        return path
+    }
+}