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

Merge branch 'dynamic-island-graph' into feature

polscm32 2 лет назад
Родитель
Сommit
a6d56528bc

+ 6 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -315,6 +315,8 @@
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.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 */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.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; };
 		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>"; };
+		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>"; };
 		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>"; };
@@ -2061,6 +2064,7 @@
 			children = (
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
+				BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
 			);
@@ -2902,6 +2906,7 @@
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
+				BD188BEC2B1B805B00B183BF /* WidgetBobble.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
@@ -3059,6 +3064,7 @@
 				6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
+				BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

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

@@ -7,6 +7,9 @@ struct LiveActivityAttributes: ActivityAttributes {
         let trendSystemImage: String?
         let change: String
         let date: Date
+        let chart: [Int16]
+        let chartDate: [Date?]
+        let rotationDegrees: Double
     }
 
     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))!
     }
 
-    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool) {
+    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool, chart: [Readings]) {
         guard let glucose = bg.glucose,
               bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
         else {
@@ -31,38 +31,56 @@ extension LiveActivityAttributes.ContentState {
         let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
 
         let trendString: String?
+        var rotationDegrees: Double = 0.0
         switch bg.direction {
         case .doubleUp,
              .singleUp,
              .tripleUp:
             trendString = "arrow.up"
+            rotationDegrees = -90
 
         case .fortyFiveUp:
             trendString = "arrow.up.right"
+            rotationDegrees = -45
 
         case .flat:
             trendString = "arrow.right"
+            rotationDegrees = 0
 
         case .fortyFiveDown:
             trendString = "arrow.down.right"
+            rotationDegrees = 45
 
         case .doubleDown,
              .singleDown,
              .tripleDown:
             trendString = "arrow.down"
+            rotationDegrees = 90
 
         case .notComputable,
              Optional.none,
              .rateOutOfRange,
              .some(.none):
             trendString = nil
+            rotationDegrees = 0
         }
 
         let change = prev?.glucose.map({
             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
         }
 
+//        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(
             new: bg,
             prev: latestGlucose,
-            mmol: settings.units == .mmolL
+            mmol: settings.units == .mmolL,
+            chart: fetchGlucose
         ) 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
             return

+ 92 - 29
LiveActivity/LiveActivity.swift

@@ -1,4 +1,5 @@
 import ActivityKit
+import Charts
 import SwiftUI
 import WidgetKit
 
@@ -19,46 +20,107 @@ struct LiveActivity: Widget {
     }
 
     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 {
         if context.isStale {
             Text("--")
         } 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 {
             Text("--")
         } else {
-            Text(context.state.bg)
             if let trendSystemImage = context.state.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 {
         ActivityConfiguration(for: LiveActivityAttributes.self) { context in
             // 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()
             .imageScale(.small)
             .padding(.all, 15)
             .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)
 
         } dynamicIsland: { context in
@@ -76,6 +138,7 @@ struct LiveActivity: Widget {
                 DynamicIslandExpandedRegion(.bottom) {
                     updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
                         .padding(.bottom, 5)
+                    chart(context: context).frame(height: 70)
                 }
             } compactLeading: {
                 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
+    }
+}