Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; };
B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* QuickPickBolusesManager.swift */; };
B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; };
B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* QuickPickMealsManager.swift */; };
2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */; };
374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77982F5BD8AB00E96858 /* APNSClient.swift */; };
374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; };
Expand Down Expand Up @@ -96,6 +100,7 @@
DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */; };
DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */; };
DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; };
B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* QuickPickSectionHeader.swift */; };
DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; };
DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; };
DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; };
Expand Down Expand Up @@ -195,8 +200,8 @@
DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0C92D355256000D2A63 /* LogView.swift */; };
DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */; };
DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CD2D355879000D2A63 /* LogEntry.swift */; };
DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; };
DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */; };
DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; };
DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */; };
DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */; };
DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; };
Expand Down Expand Up @@ -459,6 +464,10 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = "<group>"; };
B500000000000000000000A3 /* QuickPickBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesManager.swift; sourceTree = "<group>"; };
B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = "<group>"; };
B500000000000000000000B3 /* QuickPickMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickMealsManager.swift; sourceTree = "<group>"; };
059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingFutureCarb.swift; sourceTree = "<group>"; };
2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsCondition.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -545,6 +554,7 @@
DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = "<group>"; };
DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = "<group>"; };
DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = "<group>"; };
B500000000000000000000C1 /* QuickPickSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickSectionHeader.swift; sourceTree = "<group>"; };
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = "<group>"; };
DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = "<group>"; };
DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -643,8 +653,8 @@
DD9ED0C92D355256000D2A63 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
DD9ED0CD2D355879000D2A63 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = "<group>"; };
DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorMode.swift; sourceTree = "<group>"; };
DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = "<group>"; };
DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIncludeOption.swift; sourceTree = "<group>"; };
DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactType.swift; sourceTree = "<group>"; };
DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkHeartbeatBluetoothDevice.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1034,9 +1044,29 @@
path = Metric;
sourceTree = "<group>";
};
B500000000000000000000B5 /* QuickPickMeals */ = {
isa = PBXGroup;
children = (
B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */,
B500000000000000000000B3 /* QuickPickMealsManager.swift */,
);
path = QuickPickMeals;
sourceTree = "<group>";
};
B500000000000000000000A5 /* QuickPickBoluses */ = {
isa = PBXGroup;
children = (
B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */,
B500000000000000000000A3 /* QuickPickBolusesManager.swift */,
);
path = QuickPickBoluses;
sourceTree = "<group>";
};
DD0C0C6E2C4AFFB800DBADDF /* Remote */ = {
isa = PBXGroup;
children = (
B500000000000000000000A5 /* QuickPickBoluses */,
B500000000000000000000B5 /* QuickPickMeals */,
DDDF6F4A2D479B6A00884336 /* Nightscout */,
DDDF6F482D479AEF00884336 /* NoRemoteView.swift */,
DDEF503E2D479B8A00884336 /* LoopAPNS */,
Expand Down Expand Up @@ -1392,6 +1422,7 @@
DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */,
DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */,
DD16AF102C997B4600FB655A /* LoadingButtonView.swift */,
B500000000000000000000C1 /* QuickPickSectionHeader.swift */,
DDE75D262DE5E539007C1FC1 /* ActionRow.swift */,
654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */,
DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */,
Expand Down Expand Up @@ -2160,6 +2191,7 @@
DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */,
6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */,
DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */,
B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */,
DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */,
DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */,
FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */,
Expand All @@ -2186,6 +2218,10 @@
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */,
DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */,
B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */,
B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */,
B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */,
DD4878172C7B75350048F05C /* BolusView.swift in Sources */,
DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */,
374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */,
Expand Down
69 changes: 69 additions & 0 deletions LoopFollow/Helpers/Views/QuickPickSectionHeader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// LoopFollow
// QuickPickSectionHeader.swift

import SwiftUI

struct QuickPickSectionHeader: View {
let title: String
let infoText: String
@State private var showInfo = false

var body: some View {
HStack(spacing: 4) {
Text(title)
Button {
showInfo = true
} label: {
Image(systemName: "info.circle")
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.plain)
}
.sheet(isPresented: $showInfo) {
QuickPickInfoSheet(title: title, text: infoText)
}
}
}

private struct QuickPickInfoSheet: View {
let title: String
let text: String
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationStack {
ScrollView {
Text(text)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { dismiss() }
}
}
}
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}

extension QuickPickSectionHeader {
static let bolusInfoText = """
These buttons show your most-used recent bolus amounts.

They're based on what you've sent before at similar times on similar days — so if you usually give 4 units before breakfast on weekdays, that button will show up on weekday mornings.

Tap a button to fill in the amount. Nothing is sent until you review and confirm.
"""

static let mealInfoText = """
These buttons show your most-used recent meals.

They're based on what you've sent before at similar times on similar days — so if you usually send the same breakfast on weekday mornings, it'll appear as an option.

Tap a button to fill in the details. Nothing is sent until you review and confirm.
"""
}
33 changes: 33 additions & 0 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct LoopAPNSBolusView: View {
@State private var alertMessage = ""
@State private var alertType: AlertType = .success

@ObservedObject private var quickPickBoluses = QuickPickBolusesManager.shared
@FocusState private var insulinFieldIsFocused: Bool

// Add state for recommended bolus and warning
Expand Down Expand Up @@ -72,6 +73,30 @@ struct LoopAPNSBolusView: View {
}
}

if !quickPickBoluses.quickPickBoluses.isEmpty {
Section(header: QuickPickSectionHeader(title: "Quick-Pick Boluses", infoText: QuickPickSectionHeader.bolusInfoText)) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(quickPickBoluses.quickPickBoluses) { bolus in
Button {
insulinAmount = HKQuantity(unit: .internationalUnit(), doubleValue: bolus.units)
} label: {
Text("\(InsulinFormatter.shared.string(bolus.units))U")
.font(.subheadline.weight(.medium))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(Color.accentColor.opacity(0.15))
.foregroundColor(.accentColor)
.cornerRadius(8)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 4)
}
}
}

Section {
HKQuantityInputView(
label: "Insulin Amount",
Expand Down Expand Up @@ -181,6 +206,10 @@ struct LoopAPNSBolusView: View {
showAlert = true
}

let step = Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())
let maxBolus = Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit())
quickPickBoluses.refresh(stepIncrement: max(0.001, step), maxBolus: maxBolus)

loadRecommendedBolus()
// Reset timer state so it shows '-' until first tick
otpTimeRemaining = nil
Expand Down Expand Up @@ -369,6 +398,10 @@ struct LoopAPNSBolusView: View {
DispatchQueue.main.async {
self.isLoading = false
if success {
let sentUnits = insulinAmount.doubleValue(for: .internationalUnit())
if sentUnits > 0 {
QuickPickBolusesManager.shared.recordBolus(units: sentUnits)
}
// Mark TOTP code as used
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
self.alertMessage = "Insulin sent successfully!"
Expand Down
35 changes: 35 additions & 0 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct LoopAPNSCarbsView: View {
private typealias AbsorptionPreset = (hours: Int, minutes: Int)

@Environment(\.presentationMode) var presentationMode
@ObservedObject private var quickPickMeals = QuickPickMealsManager.shared
@State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0)
@State private var absorptionHours = 3
@State private var absorptionMinutes = 0
Expand Down Expand Up @@ -102,6 +103,30 @@ struct LoopAPNSCarbsView: View {
NavigationView {
VStack {
Form {
if !quickPickMeals.quickPickMeals.isEmpty {
Section(header: QuickPickSectionHeader(title: "Quick-Pick Meals", infoText: QuickPickSectionHeader.mealInfoText)) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(quickPickMeals.quickPickMeals) { meal in
Button {
carbsAmount = HKQuantity(unit: .gram(), doubleValue: meal.carbs)
} label: {
Text("\(Int(meal.carbs))g")
.font(.subheadline.weight(.medium))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(Color.accentColor.opacity(0.15))
.foregroundColor(.accentColor)
.cornerRadius(8)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 4)
}
}
}

Section {
HKQuantityInputView(
label: "Carbs Amount",
Expand Down Expand Up @@ -379,6 +404,12 @@ struct LoopAPNSCarbsView: View {
alertType = .error
showAlert = true
}

quickPickMeals.refresh(
maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()),
includeFatProtein: false
)

// Reset timer state so it shows '-' until first tick
otpTimeRemaining = nil
// Don't reset TOTP usage flag here - let the timer handle it
Expand Down Expand Up @@ -529,6 +560,10 @@ struct LoopAPNSCarbsView: View {
DispatchQueue.main.async {
self.isLoading = false
if success {
let sentCarbs = carbsAmount.doubleValue(for: .gram())
if sentCarbs > 0 {
QuickPickMealsManager.shared.recordMeal(carbs: sentCarbs)
}
// Mark TOTP code as used
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
let timeFormatter = DateFormatter()
Expand Down
Loading