From aa7a852c9abde0353310d3c34fefdd000f681e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Fri, 26 Jul 2024 21:27:08 +0200 Subject: [PATCH 01/27] Configuration profiles --- .../Core_Data.xcdatamodel/contents | 16 +- FreeAPS.xcodeproj/project.pbxproj | 138 ++- .../xcshareddata/xcschemes/FreeAPS X.xcscheme | 5 + .../xcshareddata/swiftpm/Package.resolved | 12 +- .../Resources/javascript/prepare/profile.js | 4 - .../defaults/freeaps/freeaps_settings.json | 124 +- .../Resources/json/defaults/preferences.json | 101 +- FreeAPS/Sources/APS/OpenAPS/Constants.swift | 5 + FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift | 21 +- .../Sources/APS/OpenAPS/TotalDailyDose.swift | 3 +- .../Sources/APS/Storage/CoreDataStorage.swift | 128 ++ FreeAPS/Sources/Helpers/Token.swift | 14 + FreeAPS/Sources/Logger/Logger.swift | 5 +- FreeAPS/Sources/Models/Configs.swift | 2 + FreeAPS/Sources/Models/DatabaseModels.swift | 100 ++ FreeAPS/Sources/Models/FreeAPSSettings.swift | 3 +- .../Models/NightscoutPreferences.swift | 7 - .../Sources/Models/NightscoutSettings.swift | 7 - .../Sources/Models/NightscoutStatistics.swift | 7 - FreeAPS/Sources/Models/NightscoutStatus.swift | 1 + FreeAPS/Sources/Models/Preferences.swift | 2 +- .../BolusCalculatorStateModel.swift | 26 +- .../View/BolusCalculatorConfigRootView.swift | 5 +- .../Sources/Modules/Home/HomeStateModel.swift | 4 + .../Modules/Home/View/HomeRootView.swift | 15 +- .../NightscoutConfigStateModel.swift | 4 +- .../View/NotificationsConfigRootView.swift | 4 +- .../ProfilePicker/ProfilePickerDataFlow.swift | 5 + .../ProfilePicker/ProfilePickerProvider.swift | 6 + .../ProfilePickerStateModel.swift | 46 + .../View/ProfilePickerRootView.swift | 172 +++ .../PumpSettingsEditorStateModel.swift | 1 + .../Modules/Restore/RestoreDataFlow.swift | 5 + .../Modules/Restore/RestoreProvider.swift | 6 + .../Modules/Restore/RestoreStateModel.swift | 92 ++ .../Restore/View/RestoreRootView.swift | 1073 +++++++++++++++++ .../Modules/Settings/SettingsStateModel.swift | 12 +- .../Settings/View/SettingsRootView.swift | 66 +- FreeAPS/Sources/Router/Screen.swift | 6 + .../Sources/Services/Network/Database.swift | 422 +++++++ .../Services/Network/NightscoutAPI.swift | 44 - .../Services/Network/NightscoutManager.swift | 407 +++++-- .../Services/Storage/FileStorage.swift | 12 +- FreeAPS/Sources/Views/TagCloudView.swift | 4 +- 44 files changed, 2776 insertions(+), 366 deletions(-) create mode 100644 FreeAPS/Sources/Helpers/Token.swift create mode 100644 FreeAPS/Sources/Models/DatabaseModels.swift delete mode 100644 FreeAPS/Sources/Models/NightscoutPreferences.swift delete mode 100644 FreeAPS/Sources/Models/NightscoutSettings.swift delete mode 100644 FreeAPS/Sources/Models/NightscoutStatistics.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreProvider.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift create mode 100644 FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift create mode 100644 FreeAPS/Sources/Services/Network/Database.swift diff --git a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents index 4e30b53910..0f34a2efc9 100644 --- a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents +++ b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents @@ -1,5 +1,10 @@ - + + + + + + @@ -71,6 +76,10 @@ + + + + @@ -126,6 +135,11 @@ + + + + + diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index ec304db44a..7b404ab19a 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -21,9 +21,14 @@ 190F8CF72BC6F70800EDB473 /* IllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190F8CF62BC6F70800EDB473 /* IllustrationView.swift */; }; 191A9D162BED00A500028D48 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191A9D152BED00A500028D48 /* Version.swift */; }; 191A9D182BED24B000028D48 /* ActiveIOBView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191A9D172BED24B000028D48 /* ActiveIOBView.swift */; }; - 191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */; }; + 191DF1502C3C113F003E36F6 /* RestoreDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */; }; + 191DF1522C3C1152003E36F6 /* RestoreProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */; }; + 191DF1542C3C116E003E36F6 /* RestoreStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */; }; + 191DF1562C3C1185003E36F6 /* RestoreRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */; }; 1920BF5D2B9DF53200E861FE /* BolusShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1920BF5C2B9DF53200E861FE /* BolusShortcut.swift */; }; 19229B962AFBB84800CD91CA /* Predictions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19229B952AFBB84800CD91CA /* Predictions.swift */; }; + 1922ACBD2C30B25300B28CF3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1922ACBC2C30B25300B28CF3 /* Database.swift */; }; + 192365422C4FE6EB0038AFC4 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192365412C4FE6EB0038AFC4 /* Token.swift */; }; 192424CB2B7A64E70063CBF0 /* NIghtscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */; }; 1924F72C2BA35AE5006644EE /* TotalDailyDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1924F72B2BA35AE5006644EE /* TotalDailyDose.swift */; }; 1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; }; @@ -31,8 +36,13 @@ 1935364028496F7D001E0B16 /* Dynamic structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Dynamic structs.swift */; }; 193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; }; 194297512B815938006B8A0B /* OverridesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194297502B815938006B8A0B /* OverridesView.swift */; }; + 19462AA62C396436009AA396 /* ProfilePickerDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */; }; + 19462AA82C39644E009AA396 /* ProfilePickerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */; }; + 19462AAA2C396463009AA396 /* ProfilePickerStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */; }; + 19462AAC2C39647D009AA396 /* ProfilePickerRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */; }; 194C32772B93A9BF0016FB2A /* OverrideShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C32762B93A9BF0016FB2A /* OverrideShortcuts.swift */; }; 194D7E6E2B974F9F007A38C1 /* LoopsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */; }; + 1955B1EA2C344E950054B0DA /* DatabaseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1955B1E92C344E950054B0DA /* DatabaseModels.swift */; }; 1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */; }; 195D80B42AF6973A00D25097 /* DynamicRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B32AF6973A00D25097 /* DynamicRootView.swift */; }; 195D80B72AF697B800D25097 /* DynamicDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */; }; @@ -450,8 +460,6 @@ F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */; }; F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D5274B9A450037068D /* HealthKitStateModel.swift */; }; FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */; }; - FE41E4D429463C660047FD55 /* NightscoutStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */; }; - FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */; }; FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */; }; FEFA5C0F299F810B00765C17 /* Core_Data.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FEFA5C0D299F810B00765C17 /* Core_Data.xcdatamodeld */; }; FEFA5C11299F814A00765C17 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFA5C10299F814A00765C17 /* CoreDataStack.swift */; }; @@ -571,10 +579,15 @@ 1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; 191A9D152BED00A500028D48 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 191A9D172BED24B000028D48 /* ActiveIOBView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveIOBView.swift; sourceTree = ""; }; - 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettings.swift; sourceTree = ""; }; + 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreDataFlow.swift; sourceTree = ""; }; + 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreProvider.swift; sourceTree = ""; }; + 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreStateModel.swift; sourceTree = ""; }; + 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreRootView.swift; sourceTree = ""; }; 1920BF5C2B9DF53200E861FE /* BolusShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusShortcut.swift; sourceTree = ""; }; 192202902BAB567800B95BE8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 19229B952AFBB84800CD91CA /* Predictions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predictions.swift; sourceTree = ""; }; + 1922ACBC2C30B25300B28CF3 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; + 192365412C4FE6EB0038AFC4 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIghtscoutExercise.swift; sourceTree = ""; }; 1924F72B2BA35AE5006644EE /* TotalDailyDose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDailyDose.swift; sourceTree = ""; }; 1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -628,8 +641,13 @@ 193F1E3C2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = ""; }; 194297502B815938006B8A0B /* OverridesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridesView.swift; sourceTree = ""; }; + 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerDataFlow.swift; sourceTree = ""; }; + 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerProvider.swift; sourceTree = ""; }; + 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerStateModel.swift; sourceTree = ""; }; + 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerRootView.swift; sourceTree = ""; }; 194C32762B93A9BF0016FB2A /* OverrideShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideShortcuts.swift; sourceTree = ""; }; 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopsView.swift; sourceTree = ""; }; + 1955B1E92C344E950054B0DA /* DatabaseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseModels.swift; sourceTree = ""; }; 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = ""; }; 195D80B32AF6973A00D25097 /* DynamicRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRootView.swift; sourceTree = ""; }; 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDataFlow.swift; sourceTree = ""; }; @@ -1052,8 +1070,6 @@ F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleHealthKitRootView.swift; sourceTree = ""; }; F90692D5274B9A450037068D /* HealthKitStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitStateModel.swift; sourceTree = ""; }; FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = ""; }; - FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatistics.swift; sourceTree = ""; }; - FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutPreferences.swift; sourceTree = ""; }; FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; FEFA5C0E299F810B00765C17 /* Core_Data.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Core_Data.xcdatamodel; sourceTree = ""; }; FEFA5C10299F814A00765C17 /* CoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; @@ -1200,6 +1216,25 @@ path = Shortcuts; sourceTree = ""; }; + 191DF14D2C3C110D003E36F6 /* Restore */ = { + isa = PBXGroup; + children = ( + 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */, + 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */, + 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */, + 191DF14E2C3C1124003E36F6 /* View */, + ); + path = Restore; + sourceTree = ""; + }; + 191DF14E2C3C1124003E36F6 /* View */ = { + isa = PBXGroup; + children = ( + 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */, + ); + path = View; + sourceTree = ""; + }; 1920BF5B2B9DF4B900E861FE /* Bolus */ = { isa = PBXGroup; children = ( @@ -1224,6 +1259,25 @@ name = "Recovered References"; sourceTree = ""; }; + 19462AA32C396413009AA396 /* ProfilePicker */ = { + isa = PBXGroup; + children = ( + 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */, + 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */, + 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */, + 19462AA42C396422009AA396 /* View */, + ); + path = ProfilePicker; + sourceTree = ""; + }; + 19462AA42C396422009AA396 /* View */ = { + isa = PBXGroup; + children = ( + 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */, + ); + path = View; + sourceTree = ""; + }; 194D7E6C2B974EA4007A38C1 /* Previews */ = { isa = PBXGroup; children = ( @@ -1409,28 +1463,24 @@ 3811DE0325C9D31700A708ED /* Modules */ = { isa = PBXGroup; children = ( - F2159A472BA60A0300A0B716 /* ContactTrick */, - 19F191D92BE4F93400F6297E /* Sharing */, - 195D80B22AF696EE00D25097 /* Dynamic */, - 190EBCC229FF134900BA767D /* StatConfig */, - BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */, - 19F95FF129F10F9C00314DDC /* Stat */, - CE94597C29E9E1CD0047C9C6 /* WatchConfig */, - 19E1F7E629D0828B005C8D20 /* IconConfig */, - 19D466A129AA2B0A004D5F33 /* FPUConfig */, - F90692CD274B99850037068D /* HealthKit */, 6DC5D590658EF8B8DF94F9F5 /* AddCarbs */, A9A4C88374496B3C89058A89 /* AddTempTarget */, 672F63EEAE27400625E14BAD /* AutotuneConfig */, A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */, 3811DE0425C9D32E00A708ED /* Base */, C2C98283C436DB934D7E7994 /* Bolus */, + BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */, E8176B120B55CE89F1591542 /* Calibrations */, F75CB57ED6971B46F8756083 /* CGM */, 0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */, + F2159A472BA60A0300A0B716 /* ContactTrick */, E42231DBF0DBE2B4B92D1B15 /* CREditor */, 9E56E3626FAD933385101B76 /* DataTable */, + 195D80B22AF696EE00D25097 /* Dynamic */, + 19D466A129AA2B0A004D5F33 /* FPUConfig */, + F90692CD274B99850037068D /* HealthKit */, 3811DE2725C9D49500A708ED /* Home */, + 19E1F7E629D0828B005C8D20 /* IconConfig */, D8F047E14D567F2B5DBEFD96 /* ISFEditor */, C11D545CED3ECEB525EDEE23 /* LibreConfig */, 3811DE1A25C9D48300A708ED /* Main */, @@ -1439,11 +1489,17 @@ F66B236E00924A05D6A9F9DF /* NotificationsConfig */, 19DC677C29CA66F200FD9EC4 /* OverrideProfilesConfig */, 3E1C41D9301B7058AA7BF5EA /* PreferencesEditor */, + 19462AA32C396413009AA396 /* ProfilePicker */, 99C01B871ACAB3F32CE755C7 /* PumpConfig */, E493126EA71765130F64CCE5 /* PumpSettingsEditor */, + 191DF14D2C3C110D003E36F6 /* Restore */, 3811DE3825C9D4A100A708ED /* Settings */, + 19F191D92BE4F93400F6297E /* Sharing */, 29B478DF61BF8D270F7D8954 /* Snooze */, + 19F95FF129F10F9C00314DDC /* Stat */, + 190EBCC229FF134900BA767D /* StatConfig */, 6517011F19F244F64E1FF14B /* TargetsEditor */, + CE94597C29E9E1CD0047C9C6 /* WatchConfig */, ); path = Modules; sourceTree = ""; @@ -1585,6 +1641,7 @@ 3811DE9725C9D88300A708ED /* NightscoutManager.swift */, 38FE826925CC82DB001FF17A /* NetworkService.swift */, 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */, + 1922ACBC2C30B25300B28CF3 /* Database.swift */, ); path = Network; sourceTree = ""; @@ -1846,49 +1903,47 @@ 388E5A5925B6F0250019842D /* Models */ = { isa = PBXGroup; children = ( + CE82E02628E869DF00473A9C /* AlertEntry.swift */, 385CEAC025F2EA52002D6D5B /* Announcement.swift */, 388E5A5F25B6F2310019842D /* Autosens.swift */, 38A00B1E25FC00F7006BC0B0 /* Autotune.swift */, + 19F191E32BE686AE00F6297E /* BareMinimum.swift */, 388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */, 38D0B3B525EBE24900CB6E88 /* Battery.swift */, 382C134A25F14E3700715CE1 /* BGTargets.swift */, 3870FF4225EC13F40088248F /* BloodGlucose.swift */, 38A9260425F012D8009E3739 /* CarbRatios.swift */, 38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */, + 19D4E4EA29FC6A9F00351451 /* Charts.swift */, + 19A910352A24D6D700C8951B /* Configs.swift */, + F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */, 3811DF0125CA9FEA00A708ED /* Credentials.swift */, + 1955B1E92C344E950054B0DA /* DatabaseModels.swift */, + 1935363F28496F7D001E0B16 /* Dynamic structs.swift */, + F270F68C2BAE374C00F6D8DD /* FontTracking.swift */, + F2159A512BA60F7A00A0B716 /* FontWeight.swift */, 38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */, 383948D925CD64D500E91849 /* Glucose.swift */, + E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */, + 1967DFBD29D052C200759F30 /* Icons.swift */, 382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */, 38887CCD25F5725200944304 /* IOBEntry.swift */, + 193F6CDC2A512C8F001240FD /* Loops.swift */, + 19012CDB291D2CB900FB8210 /* LoopStats.swift */, + 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */, 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */, 389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */, 3895E4C525B9E00D00214B37 /* Preferences.swift */, 38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */, 3883583325EEB38000E024B2 /* PumpSettings.swift */, 38E989DC25F5021400C0CED0 /* PumpStatus.swift */, + CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */, 38BF021C25E7E3AF00579895 /* Reservoir.swift */, + 19B0EF2028F6D66200069496 /* Statistics.swift */, 3871F38625ED661C0013ECB5 /* Suggestion.swift */, 38A0364125ED069400FCBB52 /* TempBasal.swift */, 3871F39B25ED892B0013ECB5 /* TempTarget.swift */, 3811DE8E25C9D80400A708ED /* User.swift */, - E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */, - 1935363F28496F7D001E0B16 /* Dynamic structs.swift */, - CE82E02628E869DF00473A9C /* AlertEntry.swift */, - 19B0EF2028F6D66200069496 /* Statistics.swift */, - 19F191E32BE686AE00F6297E /* BareMinimum.swift */, - 19012CDB291D2CB900FB8210 /* LoopStats.swift */, - FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */, - FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */, - 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */, - 1967DFBD29D052C200759F30 /* Icons.swift */, - 19D4E4EA29FC6A9F00351451 /* Charts.swift */, - 19A910352A24D6D700C8951B /* Configs.swift */, - 193F6CDC2A512C8F001240FD /* Loops.swift */, - CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */, - 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */, - F2159A512BA60F7A00A0B716 /* FontWeight.swift */, - F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */, - F270F68C2BAE374C00F6D8DD /* FontTracking.swift */, 191A9D152BED00A500028D48 /* Version.swift */, ); path = Models; @@ -1916,6 +1971,7 @@ 3811DE5725C9D4D500A708ED /* ProgressBar.swift */, 3811DE5525C9D4D500A708ED /* Publisher.swift */, 38E98A3625F5509500C0CED0 /* String+Extensions.swift */, + 192365412C4FE6EB0038AFC4 /* Token.swift */, 3811DEE325CA063400A708ED /* PropertyWrappers */, E06B9119275B5EEA003C04B6 /* Array+Extension.swift */, CEB434E428B8FF5D00B70274 /* UIColor.swift */, @@ -2968,6 +3024,7 @@ 3811DEB125C9D88300A708ED /* Keychain.swift in Sources */, 382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */, 19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */, + 19462AAA2C396463009AA396 /* ProfilePickerStateModel.swift in Sources */, 194D7E6E2B974F9F007A38C1 /* LoopsView.swift in Sources */, 383948D625CD4D8900E91849 /* FileStorage.swift in Sources */, 3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */, @@ -2985,11 +3042,11 @@ 191A9D162BED00A500028D48 /* Version.swift in Sources */, 38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */, 3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */, - FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */, E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */, 388E5A5C25B6F0770019842D /* JSON.swift in Sources */, 3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */, 19DC678529CA67A400FD9EC4 /* OverrideProfilesRootView.swift in Sources */, + 19462AA82C39644E009AA396 /* ProfilePickerProvider.swift in Sources */, 389A572026079BAA00BC102F /* Interpolation.swift in Sources */, 19A910382A24EF3200C8951B /* ChartsView.swift in Sources */, 38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */, @@ -3014,6 +3071,7 @@ 3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */, CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */, 38569347270B5DFB0002C50D /* CGMType.swift in Sources */, + 191DF1562C3C1185003E36F6 /* RestoreRootView.swift in Sources */, 3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */, 384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */, 3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */, @@ -3026,6 +3084,7 @@ CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */, CECA4775298DA8310095139F /* DexcomSourceG5.swift in Sources */, 19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */, + 191DF1522C3C1152003E36F6 /* RestoreProvider.swift in Sources */, 3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */, 3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */, 19F191E02BE4F98F00F6297E /* SharingStateModel.swift in Sources */, @@ -3040,6 +3099,7 @@ BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */, CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */, 190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */, + 191DF1502C3C113F003E36F6 /* RestoreDataFlow.swift in Sources */, 3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */, 19C14F442C29807C009A7E07 /* ScrollOffset.swift in Sources */, CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */, @@ -3047,7 +3107,6 @@ 38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */, 38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */, 3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */, - 191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */, FEFA5C11299F814A00765C17 /* CoreDataStack.swift in Sources */, 3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */, 3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */, @@ -3094,10 +3153,12 @@ 72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */, E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */, 45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */, + 192365422C4FE6EB0038AFC4 /* Token.swift in Sources */, CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */, D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */, 38E98A3025F52FF700C0CED0 /* Config.swift in Sources */, CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */, + 19462AA62C396436009AA396 /* ProfilePickerDataFlow.swift in Sources */, BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */, 9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */, 19AEF4322B1F5A98006FFE8B /* TIRView.swift in Sources */, @@ -3137,18 +3198,21 @@ FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */, 63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */, CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */, + 1922ACBD2C30B25300B28CF3 /* Database.swift in Sources */, 38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */, CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */, 385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */, F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */, 38887CCE25F5725200944304 /* IOBEntry.swift in Sources */, 38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */, + 1955B1EA2C344E950054B0DA /* DatabaseModels.swift in Sources */, CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */, 195D80BB2AF6980B00D25097 /* DynamicStateModel.swift in Sources */, E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */, 38192E07261BA9960094D973 /* FetchTreatmentsManager.swift in Sources */, 19012CDC291D2CB900FB8210 /* LoopStats.swift in Sources */, 6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */, + 191DF1542C3C116E003E36F6 /* RestoreStateModel.swift in Sources */, DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */, 1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */, F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */, @@ -3196,6 +3260,7 @@ 5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */, BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */, E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */, + 19462AAC2C39647D009AA396 /* ProfilePickerRootView.swift in Sources */, 919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */, F2159A4A2BA60A6000A0B716 /* ContactTrickDataFlow.swift in Sources */, 8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */, @@ -3220,7 +3285,6 @@ BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */, F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */, C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */, - FE41E4D429463C660047FD55 /* NightscoutStatistics.swift in Sources */, 38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */, 7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */, 38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */, diff --git a/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme b/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme index ae75876b19..1f453fa9bb 100644 --- a/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme +++ b/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme @@ -66,6 +66,11 @@ value = "" isEnabled = "YES"> + + 0, (suggestion.units ?? 0) <= 0 { + let index = reasonString.endIndex + reasonString.insert(contentsOf: " SMBs Disabled.", at: index) + } + // Middleware if targetGlucose != nil, let middlewareString = readMiddleware(json: profile, variable: "mw"), middlewareString.count > 2 @@ -345,12 +358,6 @@ final class OpenAPS { } } - // SMBs Disabled? - if let required = suggestion.insulinReq, required > 0, (suggestion.units ?? 0) <= 0 { - let index = reasonString.endIndex - reasonString.insert(contentsOf: " SMBs Disabled.", at: index) - } - // Save Suggestion to CoreData coredataContext.perform { [self] in if let isf = readReason(reason: reason, variable: "ISF"), diff --git a/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift b/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift index c65b7e5b9b..e49a715d2d 100644 --- a/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift +++ b/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift @@ -48,8 +48,7 @@ final class TotalDailyDose { } func insulinToday(_ data: [PumpHistoryEvent], increment: Double) -> (bolus: Decimal, basal: Decimal, hours: Double) { - let filtered = data.filter({ $0.timestamp > Calendar.current.startOfDay(for: Date()) }) - + let filtered = data.filter({ $0.timestamp >= Calendar.current.startOfDay(for: Date()) }) return totalDailyDose(filtered, increment: increment) } diff --git a/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift b/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift index 388c72c038..9877adc0a2 100644 --- a/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift @@ -206,4 +206,132 @@ final class CoreDataStorage { } return presetsArray } + + func fetchOnbarding() -> Bool { + var firstRun = true + coredataContext.performAndWait { + let requestBool = Onboarding.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestBool.sortDescriptors = [sort] + requestBool.fetchLimit = 1 + try? firstRun = self.coredataContext.fetch(requestBool).first?.firstRun ?? true + } + return firstRun + } + + func saveOnbarding() { + coredataContext.performAndWait { [self] in + let save = Onboarding(context: self.coredataContext) + save.firstRun = false + save.date = Date.now + try? self.coredataContext.save() + } + } + + func startOnbarding() { + coredataContext.performAndWait { [self] in + let save = Onboarding(context: self.coredataContext) + save.firstRun = true + save.date = Date.now + try? self.coredataContext.save() + } + } + + func fetchSettingProfileName() -> String { + fetchActiveProfile() + } + + func fetchSettingProfileNames() -> [Profiles]? { + var presetsArray: [Profiles]? + coredataContext.performAndWait { + let requestProfiles = Profiles.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestProfiles.sortDescriptors = [sort] + try? presetsArray = self.coredataContext.fetch(requestProfiles) + } + return presetsArray + } + + func fetchUniqueSettingProfileName(_ name: String) -> Bool { + var presetsArray: Profiles? + coredataContext.performAndWait { + let requestProfiles = Profiles.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestProfiles.sortDescriptors = [sort] + requestProfiles.predicate = NSPredicate( + format: "uploaded == true && name == %@", name as String + ) + try? presetsArray = self.coredataContext.fetch(requestProfiles).first + } + return (presetsArray != nil) + } + + func saveProfileSettingName(name: String) { + coredataContext.perform { [self] in + let save = Profiles(context: self.coredataContext) + save.name = name + save.date = Date.now + try? self.coredataContext.save() + } + } + + func migrateProfileSettingName(name: String) { + coredataContext.perform { [self] in + let save = Profiles(context: self.coredataContext) + save.name = name + save.date = Date.now + save.uploaded = true + try? self.coredataContext.save() + } + } + + func profileSettingUploaded(name: String) { + var profile: String = name + if profile.isEmpty { + profile = "default" + } + + // Avoid duplicates + if !fetchUniqueSettingProfileName(name) { + coredataContext.perform { [self] in + let save = Profiles(context: self.coredataContext) + save.name = profile + save.date = Date.now + save.uploaded = true + try? self.coredataContext.save() + } + } + } + + func activeProfile(name: String) { + coredataContext.perform { [self] in + let save = ActiveProfile(context: self.coredataContext) + save.name = name + save.date = Date.now + save.active = true + try? self.coredataContext.save() + } + } + + func checkIfActiveProfile() -> Bool { + var presetsArray = [ActiveProfile]() + coredataContext.performAndWait { + let requestProfiles = ActiveProfile.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestProfiles.sortDescriptors = [sort] + try? presetsArray = self.coredataContext.fetch(requestProfiles) + } + return (presetsArray.first?.active ?? false) + } + + func fetchActiveProfile() -> String { + var presetsArray = [ActiveProfile]() + coredataContext.performAndWait { + let requestProfiles = ActiveProfile.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestProfiles.sortDescriptors = [sort] + try? presetsArray = self.coredataContext.fetch(requestProfiles) + } + return presetsArray.first?.name ?? "default" + } } diff --git a/FreeAPS/Sources/Helpers/Token.swift b/FreeAPS/Sources/Helpers/Token.swift new file mode 100644 index 0000000000..cedba82c08 --- /dev/null +++ b/FreeAPS/Sources/Helpers/Token.swift @@ -0,0 +1,14 @@ +import Foundation + +final class Token { + func getIdentifier() -> String { + let keychain = BaseKeychain() + var identfier = keychain.getValue(String.self, forKey: IAPSconfig.id) ?? "" + guard identfier.count > 1 else { + identfier = UUID().uuidString + keychain.setValue(identfier, forKey: IAPSconfig.id) + return identfier + } + return identfier + } +} diff --git a/FreeAPS/Sources/Logger/Logger.swift b/FreeAPS/Sources/Logger/Logger.swift index 9ddaec8273..1b1ae0438d 100644 --- a/FreeAPS/Sources/Logger/Logger.swift +++ b/FreeAPS/Sources/Logger/Logger.swift @@ -112,7 +112,6 @@ final class Logger { static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter) static let apsManager = Logger(category: .apsManager, reporter: baseReporter) static let nightscout = Logger(category: .nightscout, reporter: baseReporter) - static let dynamic = Logger(category: .dynamic, reporter: baseReporter) enum Category: String { case `default` @@ -131,13 +130,13 @@ final class Logger { var logger: Logger { switch self { case .default: return .default - case .service: return .service + case .dynamic, + .service: return .service case .businessLogic: return .businessLogic case .openAPS: return .openAPS case .deviceManager: return .deviceManager case .apsManager: return .apsManager case .nightscout: return .nightscout - case .dynamic: return .dynamic } } diff --git a/FreeAPS/Sources/Models/Configs.swift b/FreeAPS/Sources/Models/Configs.swift index e9c0760965..d32cccac34 100644 --- a/FreeAPS/Sources/Models/Configs.swift +++ b/FreeAPS/Sources/Models/Configs.swift @@ -60,4 +60,6 @@ extension Font { static let carbsDotFont = Font.custom("CarbsDotFont", fixedSize: 12) static let bolusDotFont = Font.custom("BolusDotFont", fixedSize: 12) static let announcementSymbolFont = Font.custom("AnnouncementSymbolFont", fixedSize: 14) + + static let settingsListed = Font.custom("settingsListed", fixedSize: 15) } diff --git a/FreeAPS/Sources/Models/DatabaseModels.swift b/FreeAPS/Sources/Models/DatabaseModels.swift new file mode 100644 index 0000000000..90108ec53e --- /dev/null +++ b/FreeAPS/Sources/Models/DatabaseModels.swift @@ -0,0 +1,100 @@ +import Foundation + +struct DatabasePumpSettings: JSON { + var report = "pumpSettings" + let settings: PumpSettings? + let enteredBy: String + let profile: String? +} + +struct DatabaseTempTargets: JSON { + var report = "tempTargets" + let tempTargets: [TempTarget] + let enteredBy: String + let profile: String? +} + +struct DatabaseProfileStore: JSON { + var report = "profiles" + let units: String + var enteredBy: String + let store: [String: ScheduledNightscoutProfile] + var profile: String +} + +struct NightscoutStatistics: JSON { + var report = "statistics" + let dailystats: Statistics? + let justVersion: BareMinimum? +} + +struct NightscoutPreferences: JSON { + var report = "preferences" + let preferences: Preferences? + let enteredBy: String + let profile: String? +} + +struct NightscoutSettings: JSON { + var report = "settings" + let settings: FreeAPSSettings? + let enteredBy: String + let profile: String? +} + +struct Loaded { + var sens = false + var settings = false + var preferences = false + var targets = false + var carbratios = false + var basalProfiles = false +} + +struct ProfileList: JSON { + var profiles: String +} + +struct MigratedMeals: Codable { + var carbs: Decimal + var dish: String + var fat: Decimal + var protein: Decimal +} + +struct MigratedOverridePresets: Codable { + var advancedSettings: Bool + var cr: Bool + var date: Date + var duration: Decimal + var emoji: String + var end: Decimal + var id: String + var indefininite: Bool + var isf: Bool + var isndAndCr: Bool + var maxIOB: Decimal + var name: String + var overrideMaxIOB: Bool + var percentage: Double + var smbAlwaysOff: Bool + var smbIsOff: Bool + var smbMinutes: Decimal + var start: Decimal + var target: Decimal + var uamMinutes: Decimal +} + +struct MealDatabase: JSON { + var report = "mealPresets" + var profile: String + var presets: [MigratedMeals] + let enteredBy: String +} + +struct OverrideDatabase: JSON { + var report = "overridePresets" + var profile: String + var presets: [MigratedOverridePresets] + let enteredBy: String +} diff --git a/FreeAPS/Sources/Models/FreeAPSSettings.swift b/FreeAPS/Sources/Models/FreeAPSSettings.swift index 0fac28d7ca..023e6a42ab 100644 --- a/FreeAPS/Sources/Models/FreeAPSSettings.swift +++ b/FreeAPS/Sources/Models/FreeAPSSettings.swift @@ -65,8 +65,7 @@ struct FreeAPSSettings: JSON, Equatable { var disableCGMError: Bool = true var uploadVersion: Bool = true var skipGlucoseChart: Bool = false - var birthDate = Date.distantPast - // var sex: Sex = .secret + var birthDate: Date = .distantPast var sexSetting: Int = 3 } diff --git a/FreeAPS/Sources/Models/NightscoutPreferences.swift b/FreeAPS/Sources/Models/NightscoutPreferences.swift deleted file mode 100644 index f02213772f..0000000000 --- a/FreeAPS/Sources/Models/NightscoutPreferences.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct NightscoutPreferences: JSON { - var report = "preferences" - let preferences: Preferences? - let enteredBy: String -} diff --git a/FreeAPS/Sources/Models/NightscoutSettings.swift b/FreeAPS/Sources/Models/NightscoutSettings.swift deleted file mode 100644 index 5a757372a7..0000000000 --- a/FreeAPS/Sources/Models/NightscoutSettings.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct NightscoutSettings: JSON { - var report = "settings" - let settings: FreeAPSSettings? - let enteredBy: String -} diff --git a/FreeAPS/Sources/Models/NightscoutStatistics.swift b/FreeAPS/Sources/Models/NightscoutStatistics.swift deleted file mode 100644 index e743c78ba6..0000000000 --- a/FreeAPS/Sources/Models/NightscoutStatistics.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct NightscoutStatistics: JSON { - let report = "statistics" - let dailystats: Statistics? - let justVersion: BareMinimum? -} diff --git a/FreeAPS/Sources/Models/NightscoutStatus.swift b/FreeAPS/Sources/Models/NightscoutStatus.swift index 4102dde9d2..4cae2e9088 100644 --- a/FreeAPS/Sources/Models/NightscoutStatus.swift +++ b/FreeAPS/Sources/Models/NightscoutStatus.swift @@ -52,4 +52,5 @@ struct NightscoutProfileStore: JSON { let units: String var enteredBy: String let store: [String: ScheduledNightscoutProfile] + let profile: String? } diff --git a/FreeAPS/Sources/Models/Preferences.swift b/FreeAPS/Sources/Models/Preferences.swift index ee4057a3da..0bbe02781f 100644 --- a/FreeAPS/Sources/Models/Preferences.swift +++ b/FreeAPS/Sources/Models/Preferences.swift @@ -67,7 +67,7 @@ extension Preferences { case highTemptargetRaisesSensitivity = "high_temptarget_raises_sensitivity" case lowTemptargetLowersSensitivity = "low_temptarget_lowers_sensitivity" case sensitivityRaisesTarget = "sensitivity_raises_target" - case resistanceLowersTarget + case resistanceLowersTarget = "resistance_lowers_target" case advTargetAdjustments = "adv_target_adjustments" case exerciseMode = "exercise_mode" case halfBasalExerciseTarget = "half_basal_exercise_target" diff --git a/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift b/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift index 3cc9e30492..d1144ee5d0 100644 --- a/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift +++ b/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift @@ -2,6 +2,8 @@ import SwiftUI extension BolusCalculatorConfig { final class StateModel: BaseStateModel { + @Injected() private var storage: FileStorage! + @Published var overrideFactor: Decimal = 0 @Published var useCalc: Bool = true @Published var fattyMeals: Bool = false @@ -17,9 +19,7 @@ extension BolusCalculatorConfig { subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: { let value = max(min($0, 2), 0.1) overrideFactor = value - }, map: { - $0 - }) + }, map: { $0 }) subscribeSetting(\.allowBolusShortcut, on: $allowBolusShortcut) { allowBolusShortcut = $0 } subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 } subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 } @@ -45,6 +45,26 @@ extension BolusCalculatorConfig { }, map: { $0 }) + + // broadcaster.register(SettingsObserver.self, observer: self) + } + + // Temporary while testing. Replace/remove later + func checkProfileChange() { + guard let settings_ = storage.retrieveRaw(OpenAPS.FreeAPS.settings) else { return } + if let settings = FreeAPSSettings(from: settings_) { + overrideFactor = settings.overrideFactor + print("Override Factor: \(overrideFactor)") + useCalc = settings.useCalc + fattyMeals = settings.fattyMeals + fattyMealFactor = settings.fattyMealFactor + insulinReqPercentage = settings.insulinReqPercentage + displayPredictions = settings.displayPredictions + allowBolusShortcut = settings.allowBolusShortcut + allowedRemoteBolusAmount = settings.allowedRemoteBolusAmount + eventualBG = settings.eventualBG + minumimPrediction = settings.minumimPrediction + } } } } diff --git a/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift b/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift index 6e4d126b02..9c8160ea87 100644 --- a/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift +++ b/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift @@ -125,7 +125,10 @@ extension BolusCalculatorConfig { } header: { Text("iOS Shortcuts") } } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + state.checkProfileChange() + } .navigationBarTitle("Bolus Calculator") .navigationBarTitleDisplayMode(.automatic) .blur(radius: isPresented ? 5 : 0) diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index adb27961c9..254b512c29 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -538,6 +538,10 @@ extension Home { let ratio = min(c / (target + c - 100), maxValue) return (ratio * 100) } + + func getIdentifier() -> String { + Token().getIdentifier() + } } } diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index bc38ca3db4..1df459aaf0 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -363,7 +363,20 @@ extension Home { } } Spacer() - Button { state.showModal(for: .settings) } + Button { + /* if CoreDataStorage().fetchOnbarding() { + state + .showModal(for: .restore( + int: 0, + profile: "default", + inSitu: false, + id_: "", + uniqueID: state.getIdentifier() + )) + } else { */ + state.showModal(for: .settings) + // } + } label: { Image(systemName: "gear") .renderingMode(.template) diff --git a/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift b/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift index d1e26b52b6..0ecff87e8a 100644 --- a/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift +++ b/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift @@ -261,7 +261,7 @@ extension NightscoutConfig { let syncValues = basals.map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) } - // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit) + // SAVE TO STORAGE. SAVE TO PUMP (LoopKit) pump.syncBasalRateSchedule(items: syncValues) { result in switch result { case .success: @@ -272,8 +272,6 @@ extension NightscoutConfig { debug(.service, "Settings have been imported and the Basals saved to pump!") // DIA. Save if changed. let dia = fetchedProfile.dia - print("dia: " + dia.description) - print("pump dia: " + self.dia.description) if dia != self.dia, dia >= 0 { let file = PumpSettings( insulinActionCurve: dia, diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift index 23decc48ea..0423eb8264 100644 --- a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift +++ b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift @@ -100,7 +100,9 @@ extension NotificationsConfig { } } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + } .navigationBarTitle("Notifications") .navigationBarTitleDisplayMode(.automatic) } diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift new file mode 100644 index 0000000000..d2fa344a17 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift @@ -0,0 +1,5 @@ +enum ProfilePicker { + enum Config {} +} + +protocol ProfilePickerProvider: Provider {} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift new file mode 100644 index 0000000000..4f696fdd89 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift @@ -0,0 +1,6 @@ +import Combine +import Foundation + +extension ProfilePicker { + final class Provider: BaseProvider, ProfilePickerProvider {} +} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift new file mode 100644 index 0000000000..83e25096c3 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift @@ -0,0 +1,46 @@ +import Foundation +import Swinject + +extension ProfilePicker { + final class StateModel: BaseStateModel { + // @Injected() var keychain: Keychain! + + @Published var name: String = "" + @Published var backup: Bool = false + + let coreData = CoreDataStorage() + + func save(_ name_: String) { + coreData.saveProfileSettingName(name: name_) + } + + override func subscribe() { + backup = settingsManager.settings.uploadStats + } + + func getIdentifier() -> String { + Token().getIdentifier() + } + + func activeProfile(_ selectedProfile: String) { + coreData.activeProfile(name: selectedProfile) + } + + func deleteProfileFromDatabase(name: String) { + let database = Database(token: getIdentifier()) + + database.deleteProfile(name) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles \(name) deleted from database") + + case let .failure(error): + debug(.service, "Failed deleting \(name) from database. " + error.localizedDescription) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } +} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift b/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift new file mode 100644 index 0000000000..04f4660f00 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift @@ -0,0 +1,172 @@ +import Combine +import CoreData +import SwiftUI +import Swinject + +extension ProfilePicker { + struct RootView: BaseView { + let resolver: Resolver + @StateObject var state = StateModel() + + @Environment(\.managedObjectContext) var moc + @Environment(\.colorScheme) var colorScheme + + @FetchRequest( + entity: Profiles.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], + predicate: NSPredicate( + format: "name != %@", "" as String + ) + ) var profiles: FetchedResults + + @FetchRequest( + entity: ActiveProfile.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], + predicate: NSPredicate( + format: "active == true" + ) + ) var currentProfile: FetchedResults + + @State var selectedProfile = "" + @State var id = "" + @State var lifetime = Lifetime() + + var body: some View { + Form { + Section { + HStack { + Text("Current profile:").foregroundStyle(.secondary) + Spacer() + if let p = currentProfile.first { + Text(p.name ?? "default") + + if let exists = profiles.first(where: { $0.name == (p.name ?? "default") }), exists.uploaded { + Image(systemName: "cloud") + } + } else { + Text("default") + if profiles.first?.uploaded ?? false { + Image(systemName: "cloud") + } + } + } + } header: { Text("Active settings") } + + footer: { + Text( + "Updates and uploads to the cloud automatically whenever settings are changed and on a daily basis, provided backup is enabled." + ) + } + + Section { + TextField("Name", text: $state.name) + + Button("Save") { + state.save(state.name) + state.activeProfile(state.name) + upload() + }.disabled(state.name.isEmpty) + + } header: { + Text("Save as new profile") + } + + Section { + let uploaded = profiles.filter({ $0.uploaded == true }) + Section { + if profiles.isEmpty { Text("No profiles saved") + } else if profiles.first == uploaded.last, profiles.count == 1 { + Text("No other profiles saved") + } else { + ForEach(uploaded) { profile in + profilesView(for: profile) + .deleteDisabled(profile.name == "default" || profile.name == currentProfile.first?.name ?? "") + } + .onDelete(perform: removeProfile) + } + } + } header: { + HStack { + Text("Load Profile from") + Image(systemName: "cloud").textCase(nil).foregroundStyle(colorScheme == .dark ? .white : .black) + } + } + + Section { + Button("Upload now") { + // If no profiles saved yet + if (profiles.first?.name ?? "NoneXXX") == "NoneXXX" || (profiles.first?.name ?? "default" == "default") { + state.save("default") + state.activeProfile("default") + } + upload() + let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) + impactHeavy.impactOccurred() + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(state.backup ? Color(.systemBlue) : Color(.systemGray4)) + .tint(.white) + + } header: { Text("Backup now") } + + footer: { + if !state.backup { + Text("\nBackup disabled in Sharing settings").foregroundStyle(.orange).bold().textCase(nil) + } + } + } + .dynamicTypeSize(...DynamicTypeSize.xxLarge) + .onAppear { configureView() } + .navigationTitle("Profiles") + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder private func profilesView(for preset: Profiles) -> some View { + if (preset.name ?? "") == (currentProfile.first?.name ?? "BlaBlaXX") { + Text(preset.name ?? "").foregroundStyle(.secondary) + } else { + Text(preset.name ?? "") + .foregroundStyle(.blue) + .navigationLink(to: .restore( + int: 2, + profile: preset.name ?? "", + inSitu: true, + id_: state.getIdentifier(), + uniqueID: state.getIdentifier() + ), from: self) + .onTapGesture { + selectedProfile = preset.name ?? "" + } + } + } + + private func removeProfile(at offsets: IndexSet) { + let database = Database(token: state.getIdentifier()) + for index in offsets { + let profile = profiles[index] + + database.deleteProfile(profile.name ?? "") + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles \(profile.name ?? "") deleted from database") + self.moc.delete(profile) + do { try moc.save() } catch { /* To do: add error */ } + case let .failure(error): + debug( + .service, + "Failed deleting \(profile.name ?? "") from database. " + error.localizedDescription + ) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } + + private func upload() { + let b = BaseNightscoutManager(resolver: resolver) + b.uploadProfileAndSettings(true) + } + } +} diff --git a/FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift b/FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift index 1fb2a84c87..8368a36477 100644 --- a/FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift +++ b/FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift @@ -14,6 +14,7 @@ extension PumpSettingsEditor { maxBasal = settings.maxBasal maxBolus = settings.maxBolus dia = settings.insulinActionCurve + maxCarbs = settingsManager.settings.maxCarbs subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 } } diff --git a/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift b/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift new file mode 100644 index 0000000000..7154f89a21 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift @@ -0,0 +1,5 @@ +enum Restore { + enum Config {} +} + +protocol RestoreProvider: Provider {} diff --git a/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift b/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift new file mode 100644 index 0000000000..d5e68a8f4a --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift @@ -0,0 +1,6 @@ +import Combine +import Foundation + +extension Restore { + final class Provider: BaseProvider, RestoreProvider {} +} diff --git a/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift b/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift new file mode 100644 index 0000000000..ad55dda713 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift @@ -0,0 +1,92 @@ +import Foundation +import SwiftUI +import Swinject + +extension Restore { + final class StateModel: BaseStateModel { + @Published var name: String = "" + @Published var backup: Bool = false + @Published var basalsSaved = false + + /* + @Published var glucoseBadge = false + @Published var glucoseNotificationsAlways = false + @Published var useAlarmSound = false + @Published var addSourceInfoToGlucoseNotifications = false + @Published var lowGlucose: Decimal = 0 + @Published var highGlucose: Decimal = 0 + @Published var carbsRequiredThreshold: Decimal = 0 + @Published var useLiveActivity = false + @Published var units: GlucoseUnits = .mmolL + @Published var closedLoop = false*/ + + let coreData = CoreDataStorage() + let overrrides = OverrideStorage() + let coredataContext = CoreDataStack.shared.persistentContainer.viewContext + + override func subscribe() { + backup = settingsManager.settings.uploadStats + } + + func save(_ name: String) { + coreData.saveProfileSettingName(name: name) + } + + func saveFile(_ file: JSON, filename: String) { + let s = BaseFileStorage() + s.save(file, as: filename) + } + + func activeProfile(_ selectedProfile: String) { + coreData.activeProfile(name: selectedProfile) + } + + func fetchSettingProfileNames() -> [Profiles]? { + coreData.fetchSettingProfileNames() + } + + func saveMealPresets(_ mealPresets: [MigratedMeals]) { + coredataContext.performAndWait { + for item in mealPresets { + let saveToCoreData = Presets(context: self.coredataContext) + saveToCoreData.dish = item.dish + saveToCoreData.carbs = item.carbs as NSDecimalNumber + saveToCoreData.fat = item.fat as NSDecimalNumber + saveToCoreData.protein = item.protein as NSDecimalNumber + } + try? self.coredataContext.save() + } + } + + func saveOverridePresets(_ presets: [MigratedOverridePresets]) { + coredataContext.performAndWait { + for item in presets { + let saveToCoreData = OverridePresets(context: self.coredataContext) + saveToCoreData.percentage = item.percentage + saveToCoreData.target = item.target as NSDecimalNumber + saveToCoreData.end = item.end as NSDecimalNumber + saveToCoreData.start = item.start as NSDecimalNumber + saveToCoreData.id = item.id + saveToCoreData.advancedSettings = item.advancedSettings + saveToCoreData.cr = item.cr + saveToCoreData.duration = item.duration as NSDecimalNumber + saveToCoreData.isf = item.isf + saveToCoreData.name = item.name + saveToCoreData.isfAndCr = item.isndAndCr + saveToCoreData.smbIsAlwaysOff = item.smbAlwaysOff + saveToCoreData.smbIsOff = item.smbIsOff + saveToCoreData.smbMinutes = item.smbMinutes as NSDecimalNumber + saveToCoreData.uamMinutes = item.uamMinutes as NSDecimalNumber + saveToCoreData.date = item.date + saveToCoreData.maxIOB = item.maxIOB as NSDecimalNumber + saveToCoreData.overrideMaxIOB = item.overrideMaxIOB + } + try? self.coredataContext.save() + } + } + + func getIdentifier() -> String { + Token().getIdentifier() + } + } +} diff --git a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift new file mode 100644 index 0000000000..3ad7dce868 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift @@ -0,0 +1,1073 @@ +import SwiftUI +import Swinject + +extension Restore { + struct RootView: BaseView { + let resolver: Resolver + @StateObject var state = StateModel() + + let int: Int + let profile: String + let inSitu: Bool + let id_: String + var uniqueID: String + + @Environment(\.dismiss) private var dismiss + + @FetchRequest( + entity: Presets.entity(), sortDescriptors: [] + ) var savedMeals: FetchedResults + + @FetchRequest( + entity: OverridePresets.entity(), sortDescriptors: [] + ) var overrides: FetchedResults + + @State var basals: [BasalProfileEntry]? + @State var basalsOK: Bool = false + @State var basalsSaved: Bool = false + + @State var crs: [CarbRatioEntry]? + @State var crsOK: Bool = false + @State var crsOKSaved: Bool = false + + @State var isfs: [InsulinSensitivityEntry]? + @State var isfsOK: Bool = false + @State var isfsSaved: Bool = false + + @State var settings: Preferences? + @State var settingsOK: Bool = false + @State var settingsSaved: Bool = false + + @State var freeapsSettings: FreeAPSSettings? + @State var freeapsSettingsOK: Bool = false + @State var freeapsSettingsSaved: Bool = false + + @State var profiles: NightscoutProfileStore? + @State var profilesOK: Bool = false + + @State var targets: BGTargetEntry? + @State var targetsOK: Bool = false + @State var targetsSaved: Bool = false + + @State var tempTargets: [TempTarget]? + @State var tempTargetsOK: Bool = false + @State var tempTargetsSaved: Bool = false + + @State var pumpSettings: PumpSettings? + @State var pumpSettingsOK: Bool = false + @State var pumpSettingsSaved: Bool = false + + @State var mealPresets: [MigratedMeals]? + @State var mealPresetsOK: Bool = false + @State var mealPresetsSaved: Bool = false + + @State var overridePresets: [MigratedOverridePresets]? + @State var overridePresetsOK: Bool = false + @State var overridePresetsSaved: Bool = false + + @State var diaOK: Bool = false + @State var diaSaved: Bool = false + + @State var profileList: String? + + @State var page = 0 + @State var token: String = "" + @State var lifetime = Lifetime() + + @State var errorString = "" + + var fetchedVersionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + + var GlucoseFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + return formatter + } + + var body: some View { + Form { + if page == 0 { + onboarding + } else if page == 1 { + tokenView + if token != "" { + startImportView + } + } else if page == 2 { + importedView + } else if page == 3 { + fetchingView + listFetchedView + } else if page == 4 { + savedView + } + } + .navigationTitle(!inSitu ? "Onboarding" : "Switch Configuration") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: Button("Cancel") { + close() + }) + .navigationBarItems(leading: (page > 0 && !inSitu) ? Button("Back") { page -= 1 } : nil) + .onAppear { + page = int + if inSitu { + importSettings(id: id_) + } + } + } + + private var onboarding: some View { + Section { + HStack { + Button { page += 1 } + label: { Text("Yes") } + .buttonStyle(.borderless) + .padding(.leading, 10) + + Spacer() + + Button { + close() + } + label: { Text("No") } + .buttonStyle(.borderless) + .tint(.red) + .padding(.trailing, 10) + } + } header: { + VStack { + Text("Welcome to iAPS, v\(fetchedVersionNumber)!") + .font(.previewHeadline).frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 40) + + Text("Do you have any settings you want to import?\n").font(.previewNormal) + .frame(maxWidth: .infinity, alignment: .center) + } + .textCase(nil) + .foregroundStyle(.primary) + } + footer: { + Text( + "\n\nIf you've previously made any backup of your settings and statistics to the online database you now can choose to import all of these settings to iAPS using your recovery token. The recovery token you can find in your old iAPS app in the Sharing settings.\n\nIf you don't have any settings saved to import make sure to enable the setting \"Share all statistics\" in the Sharing settings later, as this will enable daily auto backups of your current settings and statistics." + ) + .textCase(nil) + .font(.previewNormal) + } + } + + private var tokenView: some View { + Section { + TextField("Token", text: $token) + } + header: { + Text("Enter your recovery token") + .frame(maxWidth: .infinity, alignment: .center) + } + footer: { + Text("\nThe recovery token you can find on your old phone in the Sharing settings.") + .textCase(nil) + .font(.previewNormal) + } + } + + private var startImportView: some View { + Section { + Button { + importSettings(id: token) + page += 1 + } + label: { + Text("Start import").frame(maxWidth: .infinity, alignment: .center) + } + .listRowBackground(!(token == "") ? Color(.systemBlue) : Color(.systemGray4)) + .tint(.white) + } + } + + private func migrateProfiles() { + if !inSitu, (state.fetchSettingProfileNames()?.first?.name ?? "EMPTY_XXX") == "EMPTY_XXX" { + state.activeProfile("default") + changeToken(restoreToken: token) + } + } + + private func importPresets() { + if state.coreData.fetchMealPresets().isEmpty {} + + if state.overrrides.fetchProfiles().isEmpty {} + } + + private var fetchingView: some View { + Section {} header: { + Text( + !noneFetched ? + "\nConfirm the fetched settings before saving" : "No fetched settings" + ) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .center) + .textCase(nil) + .font(.previewHeadline) + } + } + + private var listFetchedView: some View { + Group { + if let profiles = profiles { + if let defaultProfiles = profiles.store["default"] { + // Basals + let basals_ = defaultProfiles.basal.map({ + basal in + BasalProfileEntry( + start: basal.time + ":00", + minutes: offset(basal.time) / 60, + rate: basal.value + ) + }) + + let units: String = freeapsSettings?.units.rawValue ?? GlucoseUnits.mmolL.rawValue + + Section { + ForEach(basals_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.rate.formatted()) + Text("U/h") + } + } + } header: { Text("Basals") } + + // CRs + Section { + let crs_ = defaultProfiles.carbratio.map({ + cr in + CarbRatioEntry(start: cr.time, offset: (cr.timeAsSeconds ?? 0) / 60, ratio: cr.value) + }) + ForEach(crs_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.ratio.formatted()) + Text("g/U") + } + } + } header: { Text("Carb Ratios") } + + // ISFs + Section { + let isfs_ = defaultProfiles.sens.map({ + isf in + InsulinSensitivityEntry( + sensitivity: isf.value, + offset: (isf.timeAsSeconds ?? 0) / 60, + start: isf.time + ) + }) + + ForEach(isfs_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.sensitivity.formatted()) + Text(units + "/U") + } + } + } header: { Text("Insulin Sensitivities") } + + // Targets + Section { + let targets_ = defaultProfiles.target_low.map({ + target in + BGTargetEntry( + low: target.value, + high: target.value, + start: target.time, + offset: (target.timeAsSeconds ?? 0) / 60 + ) + }) + + ForEach(targets_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.low.formatted()) + Text(units) + } + } + } header: { Text("Targets") } + } + } + + // Pump Settings + if let pumpSettings = pumpSettings { + Section { + HStack { + Text("Max Bolus") + Spacer() + Text(pumpSettings.maxBolus.formatted()) + Text("U") + } + HStack { + Text("Max Basal") + Spacer() + Text(pumpSettings.maxBasal.formatted()) + Text("U") + } + HStack { + Text("DIA") + Spacer() + Text(pumpSettings.insulinActionCurve.formatted()) + Text("h") + } + } header: { Text("Pump Settings") } + } + + // Temp Targets + if let tt = tempTargets, tt.isNotEmpty { + let convert: Decimal = (freeapsSettings?.units ?? GlucoseUnits.mmolL) == GlucoseUnits.mmolL ? 0.0555 : 1 + Section { + ForEach(tt, id: \.id) { target in + HStack { + Text(target.name ?? "") + Spacer() + Text("\(target.duration) min") + Spacer() + Text(GlucoseFormatter.string(from: (target.targetBottom ?? 10) * convert as NSNumber) ?? "") + Text(freeapsSettings?.units.rawValue ?? GlucoseUnits.mmolL.rawValue) + } + } + } header: { Text("Temp Targets") } + } + + if let mealPresets = mealPresets, displayCoreData { + // Meal Presets. CoreData + Section { + ForEach(mealPresets, id: \.dish) { preset in + VStack { + Text(preset.dish).foregroundStyle(.secondary) + if preset.carbs > 0 { + HStack { + Text("Carbs") + Spacer() + Text("\(preset.carbs) g") + } + } + if preset.fat > 0 { + HStack { + Text("Fat") + Spacer() + Text("\(preset.fat) g") + } + } + if preset.protein > 0 { + HStack { + Text("Protein ") + Spacer() + Text("\(preset.protein) g") + } + } + } + } + } header: { Text("CoreData Meal Presets") } + } + + if let overridePresets = overridePresets, displayCoreData { + // Override Presets. CoreData + Section { + ForEach(overridePresets, id: \.id) { preset in + HStack { + Text(preset.name) + } + } + } header: { Text("CoreData Override Presets") } + } + + // OpenAPS Settings + if let settings = settings { + Section { + Text(trim(settings.rawJSON.debugDescription)).font(.settingsListed) + } header: { Text("OpenAPS Settings") } + } + + // iAPS Settings + if let freeapsSettings = freeapsSettings { + Section { + Text(trim(freeapsSettings.rawJSON.debugDescription)).font(.settingsListed) + } header: { Text("iAPS Settings") } + } + + Button { + save() + page += 1 + migrateProfiles() + } + label: { Text(inSitu ? "Confirm" : "Save settings") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var importedView: some View { + Group { + Section { + if int == 2 { + HStack { + Text("Profile") + Spacer() + Text(profile) + }.foregroundStyle(.secondary) + } + + if basalsOK { + HStack { + Text("Basals") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if crsOK { + HStack { + Text("Carb Ratios") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if isfsOK { + HStack { + Text("Insulin Sensitivites") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if targetsOK { + HStack { + Text("Targets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if pumpSettingsOK { + HStack { + Text("Pump Settings") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if tempTargetsOK { + HStack { + Text("Temp Targets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if settingsOK { + HStack { + Text("Preferences") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if freeapsSettingsOK { + HStack { + Text("iAPS Settings") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if displayCoreData { + if mealPresetsOK { + HStack { + Text("Meal Presets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if overridePresetsOK { + HStack { + Text("Override Presets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + } + + } header: { + Text("Fetching settings...").font(.previewNormal) + } + + footer: { + !allDone ? Text("Fetching can take up to a few seconds") : nil + } + + if !allDone { + Section { + Button { + importSettings(id: inSitu ? id_ : token) + let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) + impactHeavy.impactOccurred() + } + label: { Text("Try Again") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } footer: { errorString.isNotEmpty ? Text(errorString).textCase(nil).foregroundStyle(.orange) : nil } + } + + Button { + if noneFetched { + close() + } else { + page += 1 + } + } + label: { Text("Continue") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var savedView: some View { + Group { + Section { + if basalsOK { + HStack { + Text("Basals") + Spacer() + Text(basalsSaved ? "Saved" : "No") + .foregroundStyle(basalsSaved ? Color(.darkGreen) : .red) + } + } + + if crsOK { + HStack { + Text("Carb Ratios") + Spacer() + Text(crsOKSaved ? "Saved" : "No") + .foregroundStyle(crsOKSaved ? Color(.darkGreen) : .red) + } + } + + if isfsOK { + HStack { + Text("Insulin Sensitivites") + Spacer() + Text(isfsSaved ? "Saved" : "No") + .foregroundStyle(isfsSaved ? Color(.darkGreen) : .red) + } + } + + if targetsOK { + HStack { + Text("Targets") + Spacer() + Text(targetsSaved ? "Saved" : "No") + .foregroundStyle(targetsSaved ? Color(.darkGreen) : .red) + } + } + + if pumpSettingsOK { + HStack { + Text("Pump Settings") + Spacer() + Text(pumpSettingsSaved ? "Saved" : "No") + .foregroundStyle(pumpSettingsSaved ? Color(.darkGreen) : .red) + } + } + + if tempTargetsOK { + HStack { + Text("Temp Targets") + Spacer() + Text(tempTargetsSaved ? "Saved" : "No") + .foregroundStyle(tempTargetsSaved ? Color(.darkGreen) : .red) + } + } + + if settingsOK { + HStack { + Text("Preferences") + Spacer() + Text(settingsSaved ? "Saved" : "No") + .foregroundStyle(settingsSaved ? Color(.darkGreen) : .red) + } + } + + if freeapsSettingsOK { + HStack { + Text("iAPS Settings") + Spacer() + Text(freeapsSettingsSaved ? "Saved" : "No") + .foregroundStyle(freeapsSettingsSaved ? Color(.darkGreen) : .red) + } + } + + if displayCoreData { + if mealPresetsOK { + HStack { + Text("Meal Presets") + Spacer() + Text(mealPresetsSaved ? "Saved" : "No") + .foregroundStyle(mealPresetsSaved ? Color(.darkGreen) : .red) + } + } + + if overridePresetsOK { + HStack { + Text("Override Presets") + Spacer() + Text(overridePresetsSaved ? "Saved" : "No") + .foregroundStyle(overridePresetsSaved ? Color(.darkGreen) : .red) + } + } + } + + } header: { + Text("Saving settings...").font(.previewNormal) + } + + Button { close() } + label: { Text("OK") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var allDone: Bool { + ( + basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && tempTargetsOK && + mealPresetsOK && overridePresetsOK + ) || + ( + inSitu && basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && + tempTargetsOK + ) + } + + private var noneFetched: Bool { + !basalsOK && !isfsOK && !crsOK && !freeapsSettingsOK && !settingsOK && !targetsOK && !pumpSettingsOK && + !tempTargetsOK && !mealPresetsOK && !overridePresetsOK + } + + private func importSettings(id: String) { + var profile_ = "default" + if inSitu { + profile_ = profile + } else if profile_ == "default" { + // To not overwrite any eventual other current profile with the default settings when force onbarding (or testing) + state.activeProfile("default") + } + + fetchProfiles(token: id, name: profile_) + fetchSettings(token: id, name: profile_) + fetchPreferences(token: id, name: profile_) + fetchPumpSettings(token: id, name: profile_) + fetchTempTargets(token: id, name: profile_) + // CoreData + fetchMealPresets(token: id, name: profile_) + fetchOverridePresets(token: id, name: profile_) + } + + private func addError(_ error: String) { + if errorString.isEmpty { + errorString += error + } + } + + private func close() { + onboardingDone() + if inSitu { + dismiss() + } + } + + private var displayCoreData: Bool { + !inSitu + } + + private func fetchProfiles() { + guard let profiles = profileList else { return } + let string = profiles.components(separatedBy: ",") + for item in string { + CoreDataStorage().migrateProfileSettingName(name: item) + } + } + + func changeToken(restoreToken: String) { + let newToken = state.getIdentifier() + if newToken != restoreToken { + let database = Database(token: newToken) + database.moveProfiles(token: newToken, restoreToken: restoreToken) + .sink { completion in + switch completion { + case .finished: + debug(.service, "List of profiles moved to a new token") + self.retrieveProfiles(restoreToken: newToken) + self.fetchProfiles() + case let .failure(error): + debug(.service, "Failed moving profiles to a new token " + error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } + + func retrieveProfiles(restoreToken: String) { + let database = Database(token: restoreToken) + let coreData = CoreDataStorage() + + database.fetchProfiles() + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "List of profiles fetched from database") + self.fetchProfiles() + if !coreData.checkIfActiveProfile() { + coreData.activeProfile(name: "default") + debug(.service, "default is current profile") + } + case let .failure(error): + debug(.service, "Failed fetching List of profiles from database " + error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { self.profileList = $0.profiles } + .store(in: &lifetime) + } + + private func fetchPreferences(token: String, name: String) { + let database = Database(token: token) + database.fetchPreferences(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Preferences fetched from database. Profile: \(name)") + self.settingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Preferences: " + error.localizedDescription) + } + } + receiveValue: { self.settings = $0 } + .store(in: &lifetime) + } + + private func fetchSettings(token: String, name: String) { + let database = Database(token: token) + database.fetchSettings(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Settings fetched from database. Profile: \(name)") + self.freeapsSettingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("iAPS Settings: " + error.localizedDescription) + } + } + receiveValue: { + self.freeapsSettings = $0 + } + .store(in: &lifetime) + } + + private func fetchProfiles(token: String, name: String) { + let database = Database(token: token) + database.fetchProfile(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles fetched from database. Profile: \(name)") + self.basalsOK = true + self.isfsOK = true + self.crsOK = true + self.targetsOK = true + self.diaOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + errorString += error.localizedDescription + print("Profiles: No") + } + } + receiveValue: { self.profiles = $0 + } + .store(in: &lifetime) + } + + private func fetchPumpSettings(token: String, name: String) { + let database = Database(token: token) + database.fetchPumpSettings(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Pump Settings fetched from database. Profile: \(name)") + self.pumpSettingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Pump Settings: " + error.localizedDescription) + } + } + receiveValue: { self.pumpSettings = $0 } + .store(in: &lifetime) + } + + private func fetchTempTargets(token: String, name: String) { + let database = Database(token: token) + database.fetchTempTargets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Temp Targets fetched from database. Profile: \(name)") + self.tempTargetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Temp Targets: " + error.localizedDescription) + } + } + receiveValue: { + self.tempTargets = $0.tempTargets + } + .store(in: &lifetime) + } + + private func fetchMealPresets(token: String, name: String) { + let database = Database(token: token) + database.fetchMealPressets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Meal Presets fetched from database. Profile: \(name)") + self.mealPresetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { + self.mealPresets = $0.presets + } + .store(in: &lifetime) + } + + private func fetchOverridePresets(token: String, name: String) { + let database = Database(token: token) + database.fetchOverridePressets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Override Presets fetched from database. Profile: \(name)") + self.overridePresetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { + self.overridePresets = $0.presets + } + .store(in: &lifetime) + } + + private func verifyProfiles() { + if let fetchedProfiles = profiles { + if let defaultProfiles = fetchedProfiles.store["default"] { + // Basals + let basals_ = defaultProfiles.basal.map({ + basal in + BasalProfileEntry( + start: basal.time + ":00", + minutes: self.offset(basal.time) / 60, + rate: basal.value + ) + }) + + state.saveFile(basals_, filename: OpenAPS.Settings.basalProfile) + debug(.service, "Imported Basals have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + basalsSaved = true + + // Glucoce Unit + let preferredUnit = GlucoseUnits(rawValue: defaultProfiles.units) ?? .mmolL + + // ISFs + let sensitivities = defaultProfiles.sens.map { sensitivity -> InsulinSensitivityEntry in + InsulinSensitivityEntry( + sensitivity: sensitivity.value, + offset: self.offset(sensitivity.time) / 60, + start: sensitivity.time + ) + } + + let isfs_ = InsulinSensitivities( + units: preferredUnit, + userPrefferedUnits: preferredUnit, + sensitivities: sensitivities + ) + + state.saveFile(isfs_, filename: OpenAPS.Settings.insulinSensitivities) + + debug(.service, "Imported ISFs have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + isfsSaved = true + + // CRs + let carbRatios = defaultProfiles.carbratio.map({ + cr -> CarbRatioEntry in + CarbRatioEntry( + start: cr.time, + offset: (cr.timeAsSeconds ?? 0) / 60, + ratio: cr.value + ) + }) + let crs_ = CarbRatios(units: CarbUnit.grams, schedule: carbRatios) + + state.saveFile(crs_, filename: OpenAPS.Settings.carbRatios) + debug(.service, "Imported CRs have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + crsOKSaved = true + + // Targets + let glucoseTargets = defaultProfiles.target_low.map({ + target -> BGTargetEntry in + BGTargetEntry( + low: target.value, + high: target.value, + start: target.time, + offset: (target.timeAsSeconds ?? 0) / 60 + ) + }) + let targets_ = BGTargets(units: preferredUnit, userPrefferedUnits: preferredUnit, targets: glucoseTargets) + + state.saveFile(targets_, filename: OpenAPS.Settings.bgTargets) + debug( + .service, + "Imported Targets have been saved to file storage, profile: \(fetchedProfiles.profile ?? "")." + ) + targetsSaved = true + } + } + } + + private func verifySettings() { + if let fetchedSettings = freeapsSettings { + state.saveFile(fetchedSettings, filename: OpenAPS.FreeAPS.settings) + freeapsSettingsSaved = true + debug(.service, "Imported iAPS Settings have been saved to file storage, profile: \(profile).") + } + } + + private func verifyPreferences() { + if let fetchedSettings = settings { + state.saveFile(fetchedSettings, filename: OpenAPS.Settings.preferences) + settingsSaved = true + debug(.service, "Imported Preferences have been saved to file storage, profile: \(profile).") + } + } + + private func verifyPumpSettings() { + if let fetchedSettings = pumpSettings { + state.saveFile(fetchedSettings, filename: OpenAPS.Settings.settings) + pumpSettingsSaved = true + debug(.service, "Imported Pump settings have been saved to file storage, profile: \(profile).") + } + } + + private func verifyTempTargets() { + if let fetchedTargets = tempTargets { + state.saveFile(fetchedTargets, filename: OpenAPS.Settings.tempTargets) + tempTargetsSaved = true + debug(.service, "Imported Temp targets have been saved to file storage, profile: \(profile).") + } + } + + private func verifyMealPresets() { + if let mealPresets = mealPresets, !inSitu, savedMeals.isEmpty { + state.saveMealPresets(mealPresets) + mealPresetsSaved = true + debug(.service, "Imported Meal presets have been saved to CoreData, profile: \(profile).") + } + } + + private func verifyOverridePresets() { + if let overridePresets = overridePresets, !inSitu, overrides.isEmpty { + state.saveOverridePresets(overridePresets) + overridePresetsSaved = true + debug(.service, "Imported Override presets have been saved to CoreData, profile: \(profile).") + } + } + + private func onboardingDone() { + CoreDataStorage().saveOnbarding() + } + + private func offset(_ string: String) -> Int { + let hours = Int(string.prefix(2)) ?? 0 + let minutes = Int(string.suffix(2)) ?? 0 + return ((hours * 60) + minutes) * 60 + } + + private func save() { + verifyProfiles() + verifySettings() + verifyPreferences() + verifyPumpSettings() + verifyTempTargets() + verifyMealPresets() + verifyOverridePresets() + state.activeProfile(profile) + } + + private func trim(_ string: String) -> String { + let trim = string + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\\n", with: "") + .replacingOccurrences(of: "\\", with: "") + .replacingOccurrences(of: "}", with: "") + .replacingOccurrences(of: "{", with: "") + .replacingOccurrences( + of: "\"", + with: "", + options: NSString.CompareOptions.literal, + range: nil + ) + .replacingOccurrences(of: "[", with: "\n") + .replacingOccurrences(of: "]", with: "\n") + let data = trim.components(separatedBy: ",").sorted { $0.count < $1.count } + .debugDescription.replacingOccurrences(of: ", ", with: "\n") + .replacingOccurrences(of: "[", with: "") + .replacingOccurrences(of: "]", with: "") + .replacingOccurrences(of: "\"", with: "") + + return data + } + } +} diff --git a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift index d64c08b070..79e53e12f8 100644 --- a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift +++ b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift @@ -1,3 +1,5 @@ +import Combine +import LoopKit import SwiftUI extension Settings { @@ -5,6 +7,7 @@ extension Settings { @Injected() private var broadcaster: Broadcaster! @Injected() private var fileManager: FileManager! @Injected() private var nightscoutManager: NightscoutManager! + @Injected() private var storage: FileStorage! @Published var closedLoop = false @Published var debugOptions = false @@ -25,7 +28,6 @@ extension Settings { broadcaster.register(SettingsObserver.self, observer: self) buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" // Read branch information from the branch.txt instead of infoDictionary @@ -80,6 +82,14 @@ extension Settings { func deleteOverrides() { nightscoutManager.deleteAllNSoverrrides() // For testing } + + func startOnboarding() { + CoreDataStorage().startOnbarding() + } + + func getIdentifier() -> String { + Token().getIdentifier() + } } } diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index ff19e36bd3..5f3da4e32b 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -15,7 +15,33 @@ extension Settings { ) ) var fetchedVersionNumber: FetchedResults + @FetchRequest( + entity: ActiveProfile.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var currentProfile: FetchedResults + + @FetchRequest( + entity: Onboarding.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var onboarded: FetchedResults + + private var GlucoseFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + return formatter + } + var body: some View { + if onboarded.first?.firstRun ?? true { + Restore.RootView(resolver: resolver, int: 0, profile: "default", inSitu: false, id_: "", uniqueID: "") + + } else { + settingsView + } + } + + var settingsView: some View { Form { Section { Toggle("Closed loop", isOn: $state.closedLoop) @@ -61,6 +87,11 @@ extension Settings { Text("Notifications").navigationLink(to: .notificationsConfig, from: self) } header: { Text("Services") } + Section { + Text("\(currentProfile.first?.name ?? "default")").foregroundStyle(.green).bold() + .navigationLink(to: .profiles, from: self) + } header: { Text("Configuration Profiles") } + Section { Text("Pump Settings").navigationLink(to: .pumpSettingsEditor, from: self) Text("Basal Profile").navigationLink(to: .basalProfileEditor, from: self) @@ -89,23 +120,34 @@ extension Settings { if state.debugOptions { Group { HStack { - Text("NS Upload Profile and Settings") + Text("Upload Profile and Settings") Button("Upload") { state.uploadProfileAndSettings(true) } .frame(maxWidth: .infinity, alignment: .trailing) .buttonStyle(.borderedProminent) } /* HStack { - Text("Delete All NS Overrides") - Button("Delete") { state.deleteOverrides() } - .frame(maxWidth: .infinity, alignment: .trailing) - .buttonStyle(.borderedProminent) - .tint(.red) + Text("Delete All NS Overrides") + Button("Delete") { state.deleteOverrides() } + .frame(maxWidth: .infinity, alignment: .trailing) + .buttonStyle(.borderedProminent) + .tint(.red) }*/ HStack { Toggle("Ignore flat CGM readings", isOn: $state.disableCGMError) } + + // HStack { + Text("Test Onboarding") + .navigationLink(to: .restore( + int: 0, + profile: "default", + inSitu: true, + id_: "", + uniqueID: state.getIdentifier() + ), from: self) + .foregroundStyle(.blue) } Group { Text("Preferences") @@ -147,6 +189,13 @@ extension Settings { .navigationLink(to: .configEditor(file: OpenAPS.Monitor.glucose), from: self) } + Group { + Text("Override Presets uploaded") + .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedOverridePresets), from: self) + Text("Meal Presets uploaded") + .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedMealPresets), from: self) + } + Group { Text("Target presets") .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self) @@ -177,7 +226,10 @@ extension Settings { ShareSheet(activityItems: state.logItems()) } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + state.closedLoop = state.settingsManager.settings.closedLoop // Remove later. Test + } .navigationTitle("Settings") .navigationBarItems(trailing: Button("Close", action: state.hideSettingsModal)) .navigationBarTitleDisplayMode(.inline) diff --git a/FreeAPS/Sources/Router/Screen.swift b/FreeAPS/Sources/Router/Screen.swift index 392fd2d572..62d32dd22e 100644 --- a/FreeAPS/Sources/Router/Screen.swift +++ b/FreeAPS/Sources/Router/Screen.swift @@ -37,6 +37,8 @@ enum Screen: Identifiable, Hashable { case dynamicISF case contactTrick case sharing + case profiles + case restore(int: Int, profile: String, inSitu: Bool, id_: String, uniqueID: String) var id: Int { String(reflecting: self).hashValue } } @@ -111,6 +113,10 @@ extension Screen { ContactTrick.RootView(resolver: resolver) case .sharing: Sharing.RootView(resolver: resolver) + case .profiles: + ProfilePicker.RootView(resolver: resolver) + case let .restore(int: int, profile: profile, inSitu: inSitu, id_: id_, uniqueID: uniqueID): + Restore.RootView(resolver: resolver, int: int, profile: profile, inSitu: inSitu, id_: id_, uniqueID: uniqueID) } } diff --git a/FreeAPS/Sources/Services/Network/Database.swift b/FreeAPS/Sources/Services/Network/Database.swift new file mode 100644 index 0000000000..9af01b06b3 --- /dev/null +++ b/FreeAPS/Sources/Services/Network/Database.swift @@ -0,0 +1,422 @@ +import Combine +import Foundation + +class Database { + init(token: String) { + self.token = token + } + + private enum Config { + static let sharePath = "/upload.php" + static let versionPath = "/vcheck.php" + static let download = "/download.php?token=" + static let profileList = "§ion=profile_list" + static let retryCount = 2 + static let timeout: TimeInterval = 60 + } + + let url: URL = IAPSconfig.statURL + let token: String + + private let service = NetworkService() +} + +extension Database { + func fetchPreferences(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=preferences&profile=" + name + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: Preferences.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func moveProfiles(token: String, restoreToken: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + restoreToken + "&new_token=" + token + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func fetchProfiles() -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + Config.profileList + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: ProfileList.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func fetchSettings(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=settings&profile=" + name + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .customISO8601 + + return service.run(request) + .retry(Config.retryCount) + .decode(type: FreeAPSSettings.self, decoder: decoder) + .eraseToAnyPublisher() + } + + func fetchProfile(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=profile&profile=" + name + + var request = URLRequest(url: components.url!) + request.allowsConstrainedNetworkAccess = false + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: NightscoutProfileStore.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func deleteProfile(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=profiles_delete&profile=" + name + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func fetchPumpSettings(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=pumpSettings&profile=" + name + + var request = URLRequest(url: components.url!) + request.allowsConstrainedNetworkAccess = true + request.timeoutInterval = Config.timeout + + let decoder = JSONDecoder() + + return service.run(request) + .retry(Config.retryCount) + .decode(type: PumpSettings.self, decoder: decoder) + .eraseToAnyPublisher() + } + + func fetchTempTargets(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=tempTargets&profile=" + name + + var request = URLRequest(url: components.url!) + request.allowsConstrainedNetworkAccess = true + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: DatabaseTempTargets.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func fetchMealPressets(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=mealPresets&profile=" + name + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: MealDatabase.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func fetchOverridePressets(_ name: String) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.download + token + "§ion=overridePresets&profile=" + name + + var request = URLRequest(url: components.url!) + request.allowsConstrainedNetworkAccess = true + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: OverrideDatabase.self, decoder: JSONCoding.decoder) + .eraseToAnyPublisher() + } + + func uploadSettingsToDatabase(_ profile: NightscoutProfileStore) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(profile) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploadStats(_ stats: NightscoutStatistics) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try! JSONCoding.encoder.encode(stats) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func fetchVersion() -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.versionPath + + var request = URLRequest(url: components.url!) + request.allowsConstrainedNetworkAccess = true + request.timeoutInterval = Config.timeout + + return service.run(request) + .retry(Config.retryCount) + .decode(type: Version.self, decoder: JSONCoding.decoder) + .catch { error -> AnyPublisher in + warning(.nightscout, "Version fetching error: \(error.localizedDescription) \(request)") + return Just(Version(main: "", dev: "")).setFailureType(to: Swift.Error.self).eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + func uploadPrefs(_ prefs: NightscoutPreferences) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(prefs) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(settings) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploadPumpSettings(_ settings: DatabasePumpSettings) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(settings) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploadTempTargets(_ targets: DatabaseTempTargets) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(targets) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploadMealPresets(_ presets: MealDatabase) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(presets) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + func uploaOverrridePresets(_ presets: OverrideDatabase) -> AnyPublisher { + var components = URLComponents() + components.scheme = url.scheme + components.host = url.host + components.port = url.port + components.path = Config.sharePath + + var request = URLRequest(url: components.url!) + request.timeoutInterval = Config.timeout + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + request.httpBody = try! JSONCoding.encoder.encode(presets) + request.httpMethod = "POST" + + return service.run(request) + .retry(Config.retryCount) + .map { _ in () } + .eraseToAnyPublisher() + } + + private func migrateMealPresets() -> [MigratedMeals] { + let meals = CoreDataStorage().fetchMealPresets() + return meals.map({ item -> MigratedMeals in + MigratedMeals( + carbs: (item.carbs ?? 0) as Decimal, + dish: item.dish ?? "", + fat: (item.fat ?? 0) as Decimal, + protein: (item.protein ?? 0) as Decimal + ) + }) + } + + private func migrateOverridePresets() -> [MigratedOverridePresets] { + let presets = OverrideStorage().fetchProfiles() + return presets.map({ item -> MigratedOverridePresets in + MigratedOverridePresets( + advancedSettings: item.advancedSettings, + cr: item.cr, + date: item.date ?? Date(), + duration: (item.duration ?? 0) as Decimal, + emoji: item.emoji ?? "", + end: (item.end ?? 0) as Decimal, + id: item.id ?? "", + indefininite: item.indefinite, + isf: item.isf, + isndAndCr: item.isfAndCr, + maxIOB: (item.maxIOB ?? 0) as Decimal, + name: item.name ?? "", + overrideMaxIOB: item.overrideMaxIOB, + percentage: item.percentage, + smbAlwaysOff: item.smbIsAlwaysOff, + smbIsOff: item.smbIsOff, + smbMinutes: (item.smbMinutes ?? 0) as Decimal, + start: (item.start ?? 0) as Decimal, + target: (item.target ?? 0) as Decimal, + uamMinutes: (item.uamMinutes ?? 0) as Decimal + ) + + }) + } + + func mealPresetDatabaseUpload(profile: String, token: String) -> MealDatabase { + MealDatabase(profile: profile, presets: migrateMealPresets(), enteredBy: token) + } + + func overridePresetDatabaseUpload(profile: String, token: String) -> OverrideDatabase { + OverrideDatabase(profile: profile, presets: migrateOverridePresets(), enteredBy: token) + } +} diff --git a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift index 926884acfd..8fb61fb608 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift @@ -558,50 +558,6 @@ extension NightscoutAPI { .eraseToAnyPublisher() } - func uploadPrefs(_ prefs: NightscoutPreferences) -> AnyPublisher { - let statURL = IAPSconfig.statURL - var components = URLComponents() - components.scheme = statURL.scheme - components.host = statURL.host - components.port = statURL.port - components.path = Config.sharePath - - var request = URLRequest(url: components.url!) - request.allowsConstrainedNetworkAccess = false - request.timeoutInterval = Config.timeout - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - request.httpBody = try! JSONCoding.encoder.encode(prefs) - request.httpMethod = "POST" - - return service.run(request) - .retry(Config.retryCount) - .map { _ in () } - .eraseToAnyPublisher() - } - - func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher { - let statURL = IAPSconfig.statURL - var components = URLComponents() - components.scheme = statURL.scheme - components.host = statURL.host - components.port = statURL.port - components.path = Config.sharePath - - var request = URLRequest(url: components.url!) - request.allowsConstrainedNetworkAccess = false - request.timeoutInterval = Config.timeout - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - request.httpBody = try! JSONCoding.encoder.encode(settings) - request.httpMethod = "POST" - - return service.run(request) - .retry(Config.retryCount) - .map { _ in () } - .eraseToAnyPublisher() - } - func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher { var components = URLComponents() components.scheme = url.scheme diff --git a/FreeAPS/Sources/Services/Network/NightscoutManager.swift b/FreeAPS/Sources/Services/Network/NightscoutManager.swift index 0ea68ef309..ef3b47844e 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutManager.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutManager.swift @@ -19,7 +19,7 @@ protocol NightscoutManager: GlucoseSource { func uploadManualGlucose() func uploadStatistics(dailystat: Statistics) func uploadVersion(json: BareMinimum) - func uploadPreferences(_ preferences: Preferences) + func uploadPreferences(_ preferences: NightscoutPreferences) func uploadProfileAndSettings(_: Bool) func uploadOverride(_ profile: String, _ duration: Double, _ date: Date) func deleteAnnouncements() @@ -62,12 +62,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { settingsManager.settings.uploadStats } + private var isVersionUploadEnabled: Bool { + settingsManager.settings.uploadVersion + } + private var isUploadGlucoseEnabled: Bool { settingsManager.settings.uploadGlucose } - private var isVersionUploadEnabled: Bool { - settingsManager.settings.uploadVersion + private var name: String { + CoreDataStorage().fetchSettingProfileName() } private var nightscoutAPI: NightscoutAPI? { @@ -95,6 +99,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } } + private func saveToCoreData(_ name: String) { + CoreDataStorage().profileSettingUploaded(name: name) + } + func sourceInfo() -> [String: Any]? { if let ping = ping { return [GlucoseSourceKey.nightscoutPing.rawValue: ping] @@ -477,20 +485,34 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } } - func uploadPreferences(_ preferences: Preferences) { - let prefs = NightscoutPreferences( - preferences: settingsManager.preferences, enteredBy: getIdentifier() - ) - - let nightscout = NightscoutAPI(url: IAPSconfig.statURL) - + func uploadPreferences(_ preferences: NightscoutPreferences) { + let db = Database(token: preferences.enteredBy) processQueue.async { - nightscout.uploadPrefs(prefs) + db.uploadPrefs(preferences) .sink { completion in switch completion { case .finished: - debug(.nightscout, "Preferences uploaded to database") + debug(.nightscout, "Preferences uploaded to database. Profile: \(preferences.profile ?? "")") self.storage.save(preferences, as: OpenAPS.Nightscout.uploadedPreferences) + self.saveToCoreData(preferences.profile ?? "default") + case let .failure(error): + debug(.nightscout, "Preferences failed to upload to database " + error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } + + func uploadSettings(_ settings: NightscoutSettings) { + let db = Database(token: settings.enteredBy) + processQueue.async { + db.uploadSettings(settings) + .sink { completion in + switch completion { + case .finished: + debug(.nightscout, "Settings uploaded to database. Profile: \(settings.profile ?? "")") + self.storage.save(settings, as: OpenAPS.Nightscout.uploadedSettings) + self.saveToCoreData(settings.profile ?? "default") case let .failure(error): debug(.nightscout, error.localizedDescription) } @@ -499,22 +521,70 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } } - func uploadSettings(_ settings: FreeAPSSettings) { - let sets = NightscoutSettings( - settings: settingsManager.settings, enteredBy: getIdentifier() - ) + private func uploadPumpSettingsToDatabase(_ settings: PumpSettings, token: String, name: String?) { + let upload = DatabasePumpSettings(settings: settings, enteredBy: token, profile: name) + processQueue.async { + Database(token: token).uploadPumpSettings(upload) + .sink { completion in + switch completion { + case .finished: + debug(.nightscout, "Pump settings uploaded to database. Profile: \(upload.profile ?? "")") + self.storage.save(settings, as: OpenAPS.Nightscout.uploadedPumpSettings) + self.saveToCoreData(name ?? "default") + case let .failure(error): + debug(.nightscout, "Pump settings failed to upload to database " + error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } - let nightscout = NightscoutAPI(url: IAPSconfig.statURL) + private func uploadTempTargetsToDatabase(_ targets: [TempTarget], token: String, name: String?) { + let upload = DatabaseTempTargets(tempTargets: targets, enteredBy: token, profile: name ?? "default") + processQueue.async { + Database(token: token).uploadTempTargets(upload) + .sink { completion in + switch completion { + case .finished: + debug(.nightscout, "Temp targets uploaded to database. Profile: \(upload.profile ?? "")") + self.storage.save(targets, as: OpenAPS.Nightscout.uploadedTempTargetsDatabase) + self.saveToCoreData(name ?? "default") + case let .failure(error): + debug(.nightscout, "Temp targets failed to upload to database " + error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } + private func uploadMealPresetsToDatabase(_ presets: MealDatabase, token: String) { processQueue.async { - nightscout.uploadSettings(sets) + Database(token: token).uploadMealPresets(presets) .sink { completion in switch completion { case .finished: - debug(.nightscout, "Settings uploaded to database") - self.storage.save(settings, as: OpenAPS.Nightscout.uploadedSettings) + debug(.nightscout, "Meal presets uploaded to database. Profile: \(presets.profile)") + self.storage.save(presets, as: OpenAPS.Nightscout.uploadedMealPresets) + self.saveToCoreData(presets.profile) case let .failure(error): - debug(.nightscout, error.localizedDescription) + debug(.nightscout, "Meal presets failed to upload to database " + error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } + + private func uploadOverridePresetsToDatabase(_ presets: OverrideDatabase, token: String) { + processQueue.async { + Database(token: token).uploaOverrridePresets(presets) + .sink { completion in + switch completion { + case .finished: + debug(.nightscout, "Override presets uploaded to database. Profile: \(presets.profile)") + self.storage.save(presets, as: OpenAPS.Nightscout.uploadedOverridePresets) + self.saveToCoreData(presets.profile) + case let .failure(error): + debug(.nightscout, "Override presets failed to upload to database " + error.localizedDescription) } } receiveValue: {} .store(in: &self.lifetime) @@ -626,60 +696,84 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } func uploadProfileAndSettings(_ force: Bool) { - guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) else { + var loaded = Loaded() + + // Start trying retrieving files + let sensitivities = storage.retrieveFile(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) + if sensitivities != nil { + loaded.sens = true + debug(.nightscout, "NightscoutManager uploadProfile: file insulinSensitivities loaded") + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading insulinSensitivities") - return } - guard let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) else { + + let settings = storage.retrieveFile(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) + if settings != nil { + loaded.settings = true + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading settings") - return } - guard let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) else { + + let preferences = storage.retrieveFile(OpenAPS.Settings.preferences, as: Preferences.self) + if preferences != nil { + loaded.preferences = true + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading preferences") - return } - guard let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self) else { + + let targets = storage.retrieveFile(OpenAPS.Settings.bgTargets, as: BGTargets.self) + if targets != nil { + loaded.targets = true + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading bgTargets") - return } - guard let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) else { + + let carbRatios = storage.retrieveFile(OpenAPS.Settings.carbRatios, as: CarbRatios.self) + if carbRatios != nil { + loaded.carbratios = true + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading carbRatios") - return } - guard let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) else { + + let basalProfile = storage.retrieveFile(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) + if basalProfile != nil { + loaded.basalProfiles = true + } else { debug(.nightscout, "NightscoutManager uploadProfile: error loading basalProfile") - return } - let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in + let token = getIdentifier() + + let sens = sensitivities?.sensitivities.map { item -> NightscoutTimevalue in NightscoutTimevalue( time: String(item.start.prefix(5)), value: item.sensitivity, timeAsSeconds: item.offset * 60 ) } - let target_low = targets.targets.map { item -> NightscoutTimevalue in + + let target_low = targets?.targets.map { item -> NightscoutTimevalue in NightscoutTimevalue( time: String(item.start.prefix(5)), value: item.low, timeAsSeconds: item.offset * 60 ) } - let target_high = targets.targets.map { item -> NightscoutTimevalue in + let target_high = targets?.targets.map { item -> NightscoutTimevalue in NightscoutTimevalue( time: String(item.start.prefix(5)), value: item.high, timeAsSeconds: item.offset * 60 ) } - let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in + let cr = carbRatios?.schedule.map { item -> NightscoutTimevalue in NightscoutTimevalue( time: String(item.start.prefix(5)), value: item.ratio, timeAsSeconds: item.offset * 60 ) } - let basal = basalProfile.map { item -> NightscoutTimevalue in + let basal = basalProfile?.map { item -> NightscoutTimevalue in NightscoutTimevalue( time: String(item.start.prefix(5)), value: item.rate, @@ -696,8 +790,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } var carbs_hr: Decimal = 0 - if let isf = sensitivities.sensitivities.map(\.sensitivity).first, - let cr = carbRatios.schedule.map(\.ratio).first, + if let isf = sensitivities?.sensitivities.map(\.sensitivity).first, + let cr = carbRatios?.schedule.map(\.ratio).first, isf > 0, cr > 0 { // CarbImpact -> Carbs/hr = CI [mg/dl/5min] * 12 / ISF [mg/dl/U] * CR [g/U] @@ -709,85 +803,180 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10 } - let ps = ScheduledNightscoutProfile( - dia: settingsManager.pumpSettings.insulinActionCurve, - carbs_hr: Int(carbs_hr), - delay: 0, - timezone: TimeZone.current.identifier, - target_low: target_low, - target_high: target_high, - sens: sens, - basal: basal, - carbratio: cr, - units: nsUnits - ) - let defaultProfile = "default" - - let now = Date() - let p = NightscoutProfileStore( - defaultProfile: defaultProfile, - startDate: now, - mills: Int(now.timeIntervalSince1970) * 1000, - units: nsUnits, - enteredBy: NigtscoutTreatment.local, - store: [defaultProfile: ps] - ) + if loaded.basalProfiles, loaded.carbratios, loaded.carbratios, loaded.sens, loaded.targets { + // Unknown errors, as it shouldn't happen here + guard let glucosetarget_low = target_low else { return } + guard let glucosetarget_high = target_high else { return } + guard let unwrappedSens = sens else { return } + guard let unwrappedBasal = basal else { return } + guard let unwrappedCR = cr else { return } + + let ps = ScheduledNightscoutProfile( + dia: settingsManager.pumpSettings.insulinActionCurve, + carbs_hr: Int(carbs_hr), + delay: 0, + timezone: TimeZone.current.identifier, + target_low: glucosetarget_low, + target_high: glucosetarget_high, + sens: unwrappedSens, + basal: unwrappedBasal, + carbratio: unwrappedCR, + units: nsUnits + ) + let defaultProfile = "default" + + let now = Date() + var p = NightscoutProfileStore( + defaultProfile: "default", + startDate: now, + mills: Int(now.timeIntervalSince1970) * 1000, + units: nsUnits, + enteredBy: NigtscoutTreatment.local, + store: [defaultProfile: ps], + profile: name + ) - let nightscout = NightscoutAPI(url: IAPSconfig.statURL) + let q = NightscoutProfileStore( + defaultProfile: "default", + startDate: now, + mills: Int(now.timeIntervalSince1970) * 1000, + units: nsUnits, + enteredBy: NigtscoutTreatment.local, + store: [defaultProfile: ps], + profile: name + ) + + // UPLOAD Profiles WHEN CHANGED + if let uploadedProfile = storage.retrieveFile(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self), + (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force + { + NSLog("NightscoutManager uploadProfile, no profile change") + } else { + if let ns = nightscoutAPI, isUploadEnabled { + processQueue.async { + ns.uploadProfile(q) + .sink { completion in + switch completion { + case .finished: + self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile) + debug(.nightscout, "Profile uploaded") + case let .failure(error): + debug(.nightscout, error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } + } + + // UPLOAD Profiles to database WHEN CHANGED + if let uploadedProfile = storage.retrieveFile( + OpenAPS.Nightscout.uploadedProfileToDatabase, + as: DatabaseProfileStore.self + ), + (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force + { + NSLog("NightscoutManager uploadProfile to database, no profile change") + } else { + if isStatsUploadEnabled { + p.enteredBy = getIdentifier() + processQueue.async { + Database(token: token).uploadSettingsToDatabase(p) + .sink { completion in + switch completion { + case .finished: + debug(.nightscout, "Profiles uploaded to database. Profile: \(p.profile ?? "")") + self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfileToDatabase) + case let .failure(error): + debug(.nightscout, error.localizedDescription) + } + } receiveValue: {} + .store(in: &self.lifetime) + } + } + } + } // UPLOAD PREFERNCES WHEN CHANGED - if let uploadedPreferences = storage.retrieve(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self), - uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted(), !force + if let uploadedPreferences = storage.retrieveFile(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self), + let unWrappedPreferences = preferences { - NSLog("NightscoutManager Preferences, preferences unchanged") - } else { uploadPreferences(preferences) } + if uploadedPreferences.rawJSON.sorted() != unWrappedPreferences.rawJSON.sorted() || + force + { + let prefs = NightscoutPreferences(preferences: unWrappedPreferences, enteredBy: token, profile: name) + uploadPreferences(prefs) + } else { + NSLog("NightscoutManager Preferences, preferences unchanged") + } + } else if loaded.preferences { + let prefs = NightscoutPreferences(preferences: preferences, enteredBy: token, profile: name) + uploadPreferences(prefs) + } // UPLOAD FreeAPS Settings WHEN CHANGED if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedSettings, as: FreeAPSSettings.self), - uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted(), !force + let unwrappedSettings = settings, uploadedSettings.rawJSON.sorted() == unwrappedSettings.rawJSON.sorted(), !force { NSLog("NightscoutManager Settings, settings unchanged") - } else { uploadSettings(settings) } + } else { + let sets = NightscoutSettings( + settings: settingsManager.settings, enteredBy: getIdentifier(), profile: name + ) + uploadSettings(sets) + } + + // UPLOAD PumpSettings WHEN CHANGED + if let pumpSettings = storage.retrieveFile(OpenAPS.Settings.settings, as: PumpSettings.self) { + if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedPumpSettings, as: PumpSettings.self), + uploadedSettings.rawJSON.sorted() == pumpSettings.rawJSON.sorted(), !force + { + NSLog("PumpSettings unchanged") + } else { uploadPumpSettingsToDatabase(pumpSettings, token: token, name: name) } - // UPLOAD Profiles WHEN CHANGED - if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self), - (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force - { - NSLog("NightscoutManager uploadProfile, no profile change") } else { - if let ns = nightscoutAPI, isUploadEnabled { - processQueue.async { - ns.uploadProfile(p) - .sink { completion in - switch completion { - case .finished: - self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile) - debug(.nightscout, "Profile uploaded") - case let .failure(error): - debug(.nightscout, error.localizedDescription) - } - } receiveValue: {} - .store(in: &self.lifetime) - } + debug(.nightscout, "UploadPumpSettings: error opening pump settings") + } + + // UPLOAD Temp Targets WHEN CHANGED + if let tempTargets = storage.retrieveFile(OpenAPS.FreeAPS.tempTargetsPresets, as: [TempTarget].self) { + if let uploadedTempTargets = storage.retrieve( + OpenAPS.Nightscout.uploadedTempTargetsDatabase, + as: [TempTarget].self + ), + uploadedTempTargets.rawJSON.sorted() == tempTargets.rawJSON.sorted(), !force + { + NSLog("Temp targets unchanged") + } else { uploadTempTargetsToDatabase(tempTargets, token: token, name: name) } + + } else { + debug(.nightscout, "UploadPumpSettings: error opening pump settings") + } + + // Upload Meal Presets when needed + let mealPresets = Database(token: token).mealPresetDatabaseUpload(profile: name, token: token) + if !mealPresets.presets.isEmpty { + if let uploadedMealPresets = storage.retrieveFile(OpenAPS.Nightscout.uploadedMealPresets, as: MealDatabase.self), + mealPresets.rawJSON.sorted() == uploadedMealPresets.rawJSON.sorted(), !force + { + NSLog("Meal Presets unchanged") + } else { + uploadMealPresetsToDatabase(mealPresets, token: token) } - if isStatsUploadEnabled { - var q = p - q.enteredBy = getIdentifier() - processQueue.async { - nightscout.uploadSettingsToDatabase(q) - .sink { completion in - switch completion { - case .finished: - debug(.nightscout, "Profiles uploaded to database") - if !self.isUploadEnabled { - self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile) - } - case let .failure(error): - debug(.nightscout, error.localizedDescription) - } - } receiveValue: {} - .store(in: &self.lifetime) - } + } + + // Upload Override Presets when needed + let overridePresets = Database(token: token).overridePresetDatabaseUpload(profile: name, token: token) + if !overridePresets.presets.isEmpty { + if let uploadedOverridePresets = storage.retrieveFile( + OpenAPS.Nightscout.uploadedOverridePresets, + as: OverrideDatabase.self + ), + overridePresets.rawJSON.sorted() == uploadedOverridePresets.rawJSON.sorted(), !force + { + NSLog("Override Presets unchanged") + } else { + uploadOverridePresetsToDatabase(overridePresets, token: token) } } } @@ -950,6 +1139,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs) } + private func loadFileFromStorage(name: String) -> RawJSON { + storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name) + } + private func uploadTempTargets() { uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets) } diff --git a/FreeAPS/Sources/Services/Storage/FileStorage.swift b/FreeAPS/Sources/Services/Storage/FileStorage.swift index 6332b514f1..bd29d2d532 100644 --- a/FreeAPS/Sources/Services/Storage/FileStorage.swift +++ b/FreeAPS/Sources/Services/Storage/FileStorage.swift @@ -11,11 +11,12 @@ protocol FileStorage { func remove(_ name: String) func rename(_ name: String, to newName: String) func transaction(_ exec: (FileStorage) -> Void) + func retrieveFile(_ name: String, as type: Value.Type) -> Value? func urlFor(file: String) -> URL? } -final class BaseFileStorage: FileStorage { +final class BaseFileStorage: FileStorage, Injectable { private let processQueue = DispatchQueue.markedQueue(label: "BaseFileStorage.processQueue", qos: .utility) func save(_ value: Value, as name: String) { @@ -43,6 +44,15 @@ final class BaseFileStorage: FileStorage { } } + func retrieveFile(_ name: String, as type: Value.Type) -> Value? { + if let loaded = retrieve(name, as: type) { + return loaded + } + let file = retrieveRaw(name) ?? OpenAPS.defaults(for: name) + save(file, as: name) + return retrieve(name, as: type) + } + func append(_ newValue: Value, to name: String) { processQueue.safeSync { try? Disk.append(newValue, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder) diff --git a/FreeAPS/Sources/Views/TagCloudView.swift b/FreeAPS/Sources/Views/TagCloudView.swift index f10d7edfe6..3c78008bf5 100644 --- a/FreeAPS/Sources/Views/TagCloudView.swift +++ b/FreeAPS/Sources/Views/TagCloudView.swift @@ -58,7 +58,7 @@ struct TagCloudView: View { case textTag where textTag.contains("SMB Delivery Ratio:"): return .uam case textTag where textTag.contains("Bolus"), - textTag where textTag.contains("TDD:"): + textTag where textTag.contains("Insulin 24h:"): return .purple case textTag where textTag.contains("tdd_factor"), textTag where textTag.contains("Sigmoid function"), @@ -72,6 +72,8 @@ struct TagCloudView: View { return .purple case textTag where textTag.contains("Middleware:"): return .red + case textTag where textTag.contains("Configuration:"): + return Color(.darkGreen) default: return .insulin } From 5fe626c44b7524d4c209dd11de69c2598da17c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Mon, 29 Jul 2024 18:50:16 +0200 Subject: [PATCH 02/27] Only ask to import reset settings for old iAPS users. Make the tranistion to this new iAPS version more seamless and less "scary". New Views for onboarding and import. Move the code out from Settings module. --- FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift | 2 +- .../Sources/Modules/Home/HomeStateModel.swift | 22 +++ .../Modules/Home/View/HomeRootView.swift | 134 ++++++++++++------ .../Restore/View/RestoreRootView.swift | 60 +++++++- .../Modules/Settings/SettingsStateModel.swift | 1 - .../Settings/View/SettingsRootView.swift | 14 -- 6 files changed, 167 insertions(+), 66 deletions(-) diff --git a/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift b/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift index 63c9d7b0ef..a6d42c62d7 100644 --- a/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift +++ b/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift @@ -338,7 +338,7 @@ final class OpenAPS { // Active Configuration profile let active = CoreDataStorage().fetchActiveProfile() if active != "default" { - let index = reasonString.firstIndex(of: ";") ?? reasonString.index(reasonString.startIndex, offsetBy: -1) + let index = reasonString.firstIndex(of: ";") ?? reasonString.index(reasonString.startIndex, offsetBy: 0) reasonString.insert(contentsOf: ", Configuration: \(active)", at: index) } diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index 254b512c29..e8950ca33a 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -89,6 +89,7 @@ extension Home { @Published var tdd3DaysAgo: Decimal = 0 @Published var tddActualAverage: Decimal = 0 @Published var skipGlucoseChart: Bool = false + @Published var openAPSSettings: Preferences? let coredataContext = CoreDataStack.shared.persistentContainer.viewContext @@ -300,6 +301,27 @@ extension Home { } } + func token() -> String { + Token().getIdentifier() + } + + func fetchPreferences() { + let token = token() + let database = Database(token: token) + database.fetchPreferences("default") + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Preferences fetched from database. Profile: default") + case let .failure(error): + debug(.service, error.localizedDescription) + } + } + receiveValue: { self.openAPSSettings = $0 } + .store(in: &lifetime) + } + private func setupGlucose() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 1df459aaf0..98f2bdacc0 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -48,6 +48,11 @@ extension Home { sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] ) var enactedSliderTT: FetchedResults + @FetchRequest( + entity: Onboarding.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var onboarded: FetchedResults + private var numberFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -692,64 +697,102 @@ extension Home { .background(TimeEllipse(characters: string.count)) } + func onboardingView(token: String) -> some View { + Restore.RootView( + resolver: resolver, + int: 0, + profile: "default", + inSitu: false, + id_: token, + uniqueID: token, + openAPS: nil + ) + } + + func importResetSettingsView(token: String, settings: Preferences) -> some View { + Restore.RootView( + resolver: resolver, + int: -1, + profile: "default", + inSitu: false, + id_: token, + uniqueID: token, + openAPS: settings + ) + } + var body: some View { GeometryReader { geo in - VStack(spacing: 0) { - headerView(geo) - if !state.skipGlucoseChart, scrollOffset > scrollAmount { - glucoseHeaderView() - .transition(.move(edge: .top)) + if onboarded.first?.firstRun ?? true { + let token = state.token() + if let openAPSSettings = state.openAPSSettings { + importResetSettingsView(token: token, settings: openAPSSettings) + } else { + onboardingView(token: token) } + } else { + VStack(spacing: 0) { + headerView(geo) + if !state.skipGlucoseChart, scrollOffset > scrollAmount { + glucoseHeaderView() + .transition(.move(edge: .top)) + } - ScrollView { - ScrollViewReader { _ in - LazyVStack { - chart - if state.timeSettings { timeSetting } - preview.padding(.top, state.timeSettings ? 5 : 15) - loopPreview.padding(.top, 15) - if state.iobData.count > 5 { - activeCOBView.padding(.top, 15) - activeIOBView.padding(.top, 15) + ScrollView { + ScrollViewReader { _ in + LazyVStack { + chart + if state.timeSettings { timeSetting } + preview.padding(.top, state.timeSettings ? 5 : 15) + loopPreview.padding(.top, 15) + if state.iobData.count > 5 { + activeCOBView.padding(.top, 15) + activeIOBView.padding(.top, 15) + } } + .background(GeometryReader { geo in + let offset = -geo.frame(in: .named(scrollSpace)).minY + Color.clear + .preference( + key: ScrollViewOffsetPreferenceKey.self, + value: offset + ) + }) } - .background(GeometryReader { geo in - let offset = -geo.frame(in: .named(scrollSpace)).minY - Color.clear - .preference( - key: ScrollViewOffsetPreferenceKey.self, - value: offset - ) - }) } - } - .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - scrollOffset = value - if !state.skipGlucoseChart, scrollOffset > scrollAmount { - display.toggle() + .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in + scrollOffset = value + if !state.skipGlucoseChart, scrollOffset > scrollAmount { + display.toggle() + } } + buttonPanel(geo) } - buttonPanel(geo) - } - .background( - colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white - .opacity(IAPSconfig.backgroundOpacity * 2) - ) - .ignoresSafeArea(edges: .vertical) - .overlay { - if let progress = state.bolusProgress, let amount = state.bolusAmount { - ZStack { - RoundedRectangle(cornerRadius: 15) - .fill(.gray.opacity(0.8)) - .frame(width: 320, height: 60) - bolusProgressView(progress: progress, amount: amount) + .background( + colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white + .opacity(IAPSconfig.backgroundOpacity * 2) + ) + .ignoresSafeArea(edges: .vertical) + .overlay { + if let progress = state.bolusProgress, let amount = state.bolusAmount { + ZStack { + RoundedRectangle(cornerRadius: 15) + .fill(.gray.opacity(0.8)) + .frame(width: 320, height: 60) + bolusProgressView(progress: progress, amount: amount) + } + .frame(maxWidth: .infinity, alignment: .center) + .offset(x: 0, y: -100) } - .frame(maxWidth: .infinity, alignment: .center) - .offset(x: 0, y: -100) } } } - .onAppear(perform: configureView) + .onAppear { + configureView() + if onboarded.first?.firstRun ?? true { + state.fetchPreferences() + } + } .navigationTitle("Home") .navigationBarHidden(true) .ignoresSafeArea(.keyboard) @@ -772,7 +815,6 @@ extension Home { } ) } - .onAppear(perform: configureView) } private var popup: some View { diff --git a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift index 3ad7dce868..0eaf59e5a5 100644 --- a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift +++ b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift @@ -11,6 +11,7 @@ extension Restore { let inSitu: Bool let id_: String var uniqueID: String + var openAPS: Preferences? @Environment(\.dismiss) private var dismiss @@ -87,7 +88,9 @@ extension Restore { var body: some View { Form { - if page == 0 { + if page == -1 { + importResetSettingsView + } else if page == 0 { onboarding } else if page == 1 { tokenView @@ -111,12 +114,50 @@ extension Restore { .navigationBarItems(leading: (page > 0 && !inSitu) ? Button("Back") { page -= 1 } : nil) .onAppear { page = int - if inSitu { + if inSitu, int != -1 { importSettings(id: id_) } } } + private var importResetSettingsView: some View { + Section { + HStack { + Button { + importOpenAPSOnly() + page = 2 + } + label: { Text("Yes") } + .buttonStyle(.borderless) + .padding(.leading, 10) + + Spacer() + + Button { + close() + } + label: { Text("No") } + .buttonStyle(.borderless) + .tint(.red) + .padding(.trailing, 10) + } + } header: { + VStack { + Text("Welcome to iAPS, v\(fetchedVersionNumber)!") + .font(.previewHeadline).frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 40) + + Text( + "In this new version your OpenAPS settings have been reset to default settings, due to a resolved Type error issue.\n\nFortunately you have a backup of your old OpenAPS settings in the cloud.\n\nDo you want to try to restore these settings now?\n" + ) + .font(.previewNormal) + .frame(maxWidth: .infinity, alignment: .center) + } + .textCase(nil) + .foregroundStyle(.primary) + } + } + private var onboarding: some View { Section { HStack { @@ -513,7 +554,7 @@ extension Restore { } } header: { - Text("Fetching settings...").font(.previewNormal) + Text(!allDone ? "Fetching settings..." : "Settings fetched").font(.previewNormal) } footer: { @@ -644,7 +685,7 @@ extension Restore { } } header: { - Text("Saving settings...").font(.previewNormal) + Text(!allSaved ? "Saving settings..." : "Settings saved").font(.previewNormal) } Button { close() } @@ -664,6 +705,12 @@ extension Restore { inSitu && basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && tempTargetsOK ) + || + int == -1 && settingsOK + } + + private var allSaved: Bool { + settingsSaved && int == -1 } private var noneFetched: Bool { @@ -690,6 +737,11 @@ extension Restore { fetchOverridePresets(token: id, name: profile_) } + private func importOpenAPSOnly() { + settings = openAPS + settingsOK = true + } + private func addError(_ error: String) { if errorString.isEmpty { errorString += error diff --git a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift index 79e53e12f8..58720e9102 100644 --- a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift +++ b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift @@ -7,7 +7,6 @@ extension Settings { @Injected() private var broadcaster: Broadcaster! @Injected() private var fileManager: FileManager! @Injected() private var nightscoutManager: NightscoutManager! - @Injected() private var storage: FileStorage! @Published var closedLoop = false @Published var debugOptions = false diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index 5f3da4e32b..16e8c31fda 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -20,11 +20,6 @@ extension Settings { sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] ) var currentProfile: FetchedResults - @FetchRequest( - entity: Onboarding.entity(), - sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] - ) var onboarded: FetchedResults - private var GlucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -33,15 +28,6 @@ extension Settings { } var body: some View { - if onboarded.first?.firstRun ?? true { - Restore.RootView(resolver: resolver, int: 0, profile: "default", inSitu: false, id_: "", uniqueID: "") - - } else { - settingsView - } - } - - var settingsView: some View { Form { Section { Toggle("Closed loop", isOn: $state.closedLoop) From 38759ff684b206b8801d0da8a5c6059434c5eed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:27:02 +0200 Subject: [PATCH 03/27] Update Config.xcconfig Update version number for the statistics and to make it easier to distinguish --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index e2ff2a4f76..d914bf2f6a 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -1,5 +1,5 @@ APP_DISPLAY_NAME = iAPS -APP_VERSION = 4.7.0 +APP_VERSION = 4.9.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = DEVELOPER_TEAM = ##TEAM_ID## From adc6edbb08d991f6ed7ac7d4b78de2bdf322646c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 31 Jul 2024 03:14:57 +0200 Subject: [PATCH 04/27] Check if old/new iAPS user --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 98f2bdacc0..7c2a756d4e 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -697,14 +697,14 @@ extension Home { .background(TimeEllipse(characters: string.count)) } - func onboardingView(token: String) -> some View { + func onboardingView() -> some View { Restore.RootView( resolver: resolver, int: 0, profile: "default", inSitu: false, - id_: token, - uniqueID: token, + id_: "", + uniqueID: "", openAPS: nil ) } @@ -725,10 +725,11 @@ extension Home { GeometryReader { geo in if onboarded.first?.firstRun ?? true { let token = state.token() - if let openAPSSettings = state.openAPSSettings { + // If old iAPS user + if let openAPSSettings = state.openAPSSettings, !fetchedPercent.isEmpty, !fetchedProfiles.isEmpty { importResetSettingsView(token: token, settings: openAPSSettings) } else { - onboardingView(token: token) + onboardingView() } } else { VStack(spacing: 0) { From 1790423e650bf50698273d59ff064acdaf4faccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 31 Jul 2024 03:48:53 +0200 Subject: [PATCH 05/27] Updates --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 7c2a756d4e..da30b39b68 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -725,10 +725,11 @@ extension Home { GeometryReader { geo in if onboarded.first?.firstRun ?? true { let token = state.token() - // If old iAPS user - if let openAPSSettings = state.openAPSSettings, !fetchedPercent.isEmpty, !fetchedProfiles.isEmpty { + // If old iAPS user pre v4.9.0 (not perfect yet) + if state.glucose.isNotEmpty, state.iobData.isNotEmpty, let openAPSSettings = state.openAPSSettings { importResetSettingsView(token: token, settings: openAPSSettings) } else { + // New iAPS user onboardingView() } } else { From 2f5f97aefa7e2e4745a5f13265b4e375b8cb480c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 31 Jul 2024 13:30:55 +0200 Subject: [PATCH 06/27] Make sure database uploads are complete --- FreeAPS/Sources/Services/Network/NightscoutManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FreeAPS/Sources/Services/Network/NightscoutManager.swift b/FreeAPS/Sources/Services/Network/NightscoutManager.swift index ef3b47844e..7d4bb63890 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutManager.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutManager.swift @@ -452,6 +452,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { debug(.nightscout, "Statistics uploaded") CoreDataStorage().saveStatUploadCount() UserDefaults.standard.set(false, forKey: IAPSconfig.newVersion) + self.uploadProfileAndSettings(true) case let .failure(error): debug(.nightscout, "Statistics upload failed" + error.localizedDescription) } From 8527942fe013699f19a8f182f9e22a52c83766ed Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Sun, 11 Aug 2024 17:40:39 +0200 Subject: [PATCH 07/27] Bump version --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index d914bf2f6a..9f351c73ea 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -1,5 +1,5 @@ APP_DISPLAY_NAME = iAPS -APP_VERSION = 4.9.0 +APP_VERSION = 5.3.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = DEVELOPER_TEAM = ##TEAM_ID## From 8318fe5bf5563a9794864d82a6c770a57719026f Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Mon, 12 Aug 2024 13:45:08 +0200 Subject: [PATCH 08/27] change default max carbs --- FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json index b2b9e01ec9..d354f5df67 100644 --- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json +++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json @@ -7,7 +7,7 @@ "units": "mmol/L", "timeCap": 8, "useCalc": true, - "maxCarbs": 350, + "maxCarbs": 200, "birthDate": "0001-01-01T23:00:00.000Z", "displayHR": true, "closedLoop": false, From ba7f8299be0ac2daabe31e4f5f5f45ac08a519e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Fri, 23 Aug 2024 20:35:14 +0200 Subject: [PATCH 09/27] Merge dev updates (manually due to merge conflicts). --- FreeAPS.xcodeproj/project.pbxproj | 4 +- .../Sources/Modules/Home/HomeStateModel.swift | 4 + .../Modules/Home/View/HomeRootView.swift | 283 ++++++++-------- .../Home/View/Previews/ActiveCOBView.swift | 45 ++- .../Home/View/Previews/ActiveIOBView.swift | 82 ++++- .../Home/View/Previews/LoopsView.swift | 26 +- .../Modules/Home/View/Previews/TIRView.swift | 309 ++++++++---------- 7 files changed, 389 insertions(+), 364 deletions(-) diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 4b617b817e..168f431857 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -1285,8 +1285,8 @@ children = ( 19AEF4312B1F5A98006FFE8B /* TIRView.swift */, 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */, - 191A9D172BED24B000028D48 /* ActiveIOBView.swift */, 19DB70A62BF8F01E00C05381 /* ActiveCOBView.swift */, + 191A9D172BED24B000028D48 /* ActiveIOBView.swift */, ); path = Previews; sourceTree = ""; @@ -2995,7 +2995,6 @@ 19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */, 1967DFC229D053D300759F30 /* IconImage.swift in Sources */, 382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */, - 19493A3B2C5997AD00EC83A7 /* Database.swift in Sources */, 38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */, 38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */, 38AEE75225F022080013F05B /* SettingsManager.swift in Sources */, @@ -3252,7 +3251,6 @@ 19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */, 44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */, E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */, - 19493A3D2C59987700EC83A7 /* DatabaseModels.swift in Sources */, CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */, 38E8755427561E9800975559 /* DataFlow.swift in Sources */, 38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */, diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index 2344a31e45..8a363c9626 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -81,6 +81,8 @@ extension Home { @Published var maxBolusValue: Decimal = 1 @Published var useInsulinBars: Bool = false @Published var iobData: [IOBData] = [] + @Published var carbData: Decimal = 0 + @Published var iobs: Decimal = 0 @Published var neg: Int = 0 @Published var tddChange: Decimal = 0 @Published var tddAverage: Decimal = 0 @@ -516,6 +518,8 @@ extension Home { guard let self = self else { return } if let data = self.provider.reasons() { self.iobData = data + self.carbData = data.map(\.cob).reduce(0, +) + self.iobs = data.map(\.iob).reduce(0, +) neg = data.filter({ $0.iob < 0 }).count * 5 let tdds = CoreDataStorage().fetchTDD(interval: DateFilter().tenDays) let yesterday = (tdds.first(where: { diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 580ee11e2f..830db752e5 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -16,11 +16,10 @@ extension Home { @State var triggerUpdate = false @State var scrollOffset = CGFloat.zero @State var display = false + @State var displayGlucose = false - @Namespace var scrollSpace - - let scrollAmount: CGFloat = 290 let buttonFont = Font.custom("TimeButtonFont", size: 14) + let viewPadding: CGFloat = 5 @Environment(\.managedObjectContext) var moc @Environment(\.colorScheme) var colorScheme @@ -283,115 +282,105 @@ extension Home { .frame(height: 50 + geo.safeAreaInsets.bottom) let isOverride = fetchedPercent.first?.enabled ?? false let isTarget = (state.tempTarget != nil) - HStack { - Button { state.showModal(for: .addCarbs(editMode: false, override: false)) } - label: { - ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { - Image(systemName: "fork.knife") - .renderingMode(.template) - .font(.custom("Buttons", size: 24)) - .foregroundColor(colorScheme == .dark ? .loopYellow : .orange) - .padding(8) - .foregroundColor(.loopYellow) - if let carbsReq = state.carbsRequired { - Text(numberFormatter.string(from: carbsReq as NSNumber)!) - .font(.caption) - .foregroundColor(.white) - .padding(4) - .background(Capsule().fill(Color.red)) + VStack { + Divider() + HStack { + Button { state.showModal(for: .addCarbs(editMode: false, override: false)) } + label: { + ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { + Image(systemName: "fork.knife") + .renderingMode(.template) + .font(.custom("Buttons", size: 24)) + .foregroundColor(colorScheme == .dark ? .loopYellow : .orange) + .padding(8) + .foregroundColor(.loopYellow) + if let carbsReq = state.carbsRequired { + Text(numberFormatter.string(from: carbsReq as NSNumber)!) + .font(.caption) + .foregroundColor(.white) + .padding(4) + .background(Capsule().fill(Color.red)) + } } + }.buttonStyle(.borderless) + Spacer() + Button { + state.showModal(for: .bolus( + waitForSuggestion: state.useCalc ? true : false, + fetch: false + )) } - }.buttonStyle(.borderless) - Spacer() - Button { - state.showModal(for: .bolus( - waitForSuggestion: state.useCalc ? true : false, - fetch: false - )) - } - label: { - Image(systemName: "syringe") - .renderingMode(.template) - .font(.custom("Buttons", size: 24)) - } - .buttonStyle(.borderless) - .foregroundColor(.insulin) - Spacer() - if state.allowManualTemp { - Button { state.showModal(for: .manualTempBasal) } label: { - Image("bolus1") + Image(systemName: "syringe") .renderingMode(.template) - .resizable() - .frame(width: IAPSconfig.buttonSize, height: IAPSconfig.buttonSize, alignment: .bottom) + .font(.custom("Buttons", size: 24)) } + .buttonStyle(.borderless) .foregroundColor(.insulin) Spacer() - } - ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { - Image(systemName: isOverride ? "person.fill" : "person") - .symbolRenderingMode(.palette) - .font(.custom("Buttons", size: 28)) - .foregroundStyle(.purple) - .padding(8) - .background(isOverride ? .purple.opacity(0.15) : .clear) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - .onTapGesture { - if isOverride { - showCancelAlert.toggle() - } else { + if state.allowManualTemp { + Button { state.showModal(for: .manualTempBasal) } + label: { + Image("bolus1") + .renderingMode(.template) + .resizable() + .frame(width: IAPSconfig.buttonSize, height: IAPSconfig.buttonSize, alignment: .bottom) + } + .foregroundColor(.insulin) + Spacer() + } + ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { + Image(systemName: isOverride ? "person.fill" : "person") + .symbolRenderingMode(.palette) + .font(.custom("Buttons", size: 28)) + .foregroundStyle(.purple) + .padding(8) + .background(isOverride ? .purple.opacity(0.15) : .clear) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + .onTapGesture { + if isOverride { + showCancelAlert.toggle() + } else { + state.showModal(for: .overrideProfilesConfig) + } + } + .onLongPressGesture { state.showModal(for: .overrideProfilesConfig) } - } - .onLongPressGesture { - state.showModal(for: .overrideProfilesConfig) - } - if state.useTargetButton { - Spacer() - Image(systemName: "target") - .renderingMode(.template) - .font(.custom("Buttons", size: 24)) - .padding(8) - .foregroundColor(.loopGreen) - .background(isTarget ? .green.opacity(0.15) : .clear) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .onTapGesture { - if isTarget { - showCancelTTAlert.toggle() - } else { + if state.useTargetButton { + Spacer() + Image(systemName: "target") + .renderingMode(.template) + .font(.custom("Buttons", size: 24)) + .padding(8) + .foregroundColor(.loopGreen) + .background(isTarget ? .green.opacity(0.15) : .clear) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .onTapGesture { + if isTarget { + showCancelTTAlert.toggle() + } else { + state.showModal(for: .addTempTarget) + } + } + .onLongPressGesture { state.showModal(for: .addTempTarget) } - } - .onLongPressGesture { - state.showModal(for: .addTempTarget) - } - } - Spacer() - Button { - /* if CoreDataStorage().fetchOnbarding() { - state - .showModal(for: .restore( - int: 0, - profile: "default", - inSitu: false, - id_: "", - uniqueID: state.getIdentifier() - )) - } else { */ - state.showModal(for: .settings) - // } - } - label: { - Image(systemName: "gear") - .renderingMode(.template) - .font(.custom("Buttons", size: 24)) + } + Spacer() + Button { state.showModal(for: .settings) } + label: { + Image(systemName: "gear") + .renderingMode(.template) + .font(.custom("Buttons", size: 24)) + } + .buttonStyle(.borderless) + .foregroundColor(.gray) } - .buttonStyle(.borderless) - .foregroundColor(.gray) + .padding(.horizontal, state.allowManualTemp ? 5 : 24) + .padding(.bottom, geo.safeAreaInsets.bottom) } - .padding(.horizontal, state.allowManualTemp ? 5 : 24) - .padding(.bottom, geo.safeAreaInsets.bottom) } .dynamicTypeSize(...DynamicTypeSize.xxLarge) .confirmationDialog("Cancel Profile Override", isPresented: $showCancelAlert) { @@ -411,7 +400,7 @@ extension Home { let ratio = state.timeSettings ? 1.61 : 1.44 let ratio2 = state.timeSettings ? 1.65 : 1.51 - return addColouredBackground() + return addColouredBackground().addShadows() .overlay { VStack(spacing: 0) { infoPanel @@ -486,7 +475,7 @@ extension Home { var activeIOBView: some View { addBackground() - .frame(minHeight: 430) + .frame(minHeight: 405) .overlay { ActiveIOBView( data: $state.iobData, @@ -506,7 +495,7 @@ extension Home { var activeCOBView: some View { addBackground() - .frame(minHeight: 230) + .frame(minHeight: 190) .overlay { ActiveCOBView(data: $state.iobData) } @@ -517,7 +506,7 @@ extension Home { var loopPreview: some View { addBackground() - .frame(minHeight: 190) + .frame(minHeight: 160) .overlay { LoopsView(loopStatistics: $state.loopStatistics) } @@ -592,11 +581,11 @@ extension Home { } } - @ViewBuilder private func headerView(_ geo: GeometryProxy) -> some View { + @ViewBuilder private func headerView(_ geo: GeometryProxy, extra: CGFloat) -> some View { addHeaderBackground() .frame( - maxHeight: fontSize < .extraExtraLarge ? 125 + geo.safeAreaInsets.top : 135 + geo - .safeAreaInsets.top + maxHeight: fontSize < .extraExtraLarge ? 125 + geo.safeAreaInsets.top + extra : 135 + geo + .safeAreaInsets.top + extra ) .overlay { VStack { @@ -616,23 +605,19 @@ extension Home { .dynamicTypeSize(...DynamicTypeSize.xLarge) .padding(.horizontal, 10) } - }.padding(.top, geo.safeAreaInsets.top).padding(.bottom, 10) + // Small glucose View, past 24 hours. + if extra > 0 { glucoseHeaderView() } + Divider() + }.padding(.top, geo.safeAreaInsets.top) } .clipShape(Rectangle()) } @ViewBuilder private func glucoseHeaderView() -> some View { - addHeaderBackground() - .frame(maxHeight: 90) - .overlay { - VStack { - ZStack { - glucosePreview.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .dynamicTypeSize(...DynamicTypeSize.medium) - } - } - } - .clipShape(Rectangle()) + ZStack { + glucosePreview + .dynamicTypeSize(...DynamicTypeSize.medium) + } } var glucosePreview: some View { @@ -734,40 +719,42 @@ extension Home { } } else { VStack(spacing: 0) { - headerView(geo) - if !state.skipGlucoseChart, scrollOffset > scrollAmount { - glucoseHeaderView() - .transition(.move(edge: .top)) - } - + headerView(geo, extra: (displayGlucose && !state.skipGlucoseChart) ? 93 : 0) ScrollView { - ScrollViewReader { _ in - LazyVStack { - chart - if state.timeSettings { timeSetting } - preview.padding(.top, state.timeSettings ? 5 : 15) - loopPreview.padding(.top, 15) - if state.iobData.count > 5 { - activeCOBView.padding(.top, 15) - activeIOBView.padding(.top, 15) - } + VStack { + // Main Chart + chart + + // Adjust hours visible (X-Axis) + if state.timeSettings, !displayGlucose { timeSetting } + // TIR Chart + preview.padding(.top, (state.timeSettings && !displayGlucose) ? 5 : 15).padding(.horizontal, 15) + // Loops Chart + loopPreview.padding(15) + // COB Chart + if state.carbData > 0 { + activeCOBView + } + // IOB Chart + if state.iobs > 0 { + activeIOBView.padding(.top, state.carbData > 0 ? viewPadding : 0) } - .background(GeometryReader { geo in - let offset = -geo.frame(in: .named(scrollSpace)).minY + }.background { + // Track vertical scroll + GeometryReader { proxy in + let scrollPosition = proxy.frame(in: .named("HomeScrollView")).minY + let yThreshold: CGFloat = state.timeSettings ? -500 : -560 Color.clear - .preference( - key: ScrollViewOffsetPreferenceKey.self, - value: offset - ) - }) - } - } - .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - scrollOffset = value - if !state.skipGlucoseChart, scrollOffset > scrollAmount { - display.toggle() + .onChange(of: scrollPosition) { y in + if y < yThreshold, state.iobData.count > 5, !state.skipGlucoseChart { + withAnimation(.easeOut(duration: 0.15)) { displayGlucose = true } + } else { + withAnimation(.easeOut(duration: 0.10)) { displayGlucose = false } + } + } + } } - } + }.coordinateSpace(name: "HomeScrollView") buttonPanel(geo) } .background( diff --git a/FreeAPS/Sources/Modules/Home/View/Previews/ActiveCOBView.swift b/FreeAPS/Sources/Modules/Home/View/Previews/ActiveCOBView.swift index db17ddcd4b..71f69ccae5 100644 --- a/FreeAPS/Sources/Modules/Home/View/Previews/ActiveCOBView.swift +++ b/FreeAPS/Sources/Modules/Home/View/Previews/ActiveCOBView.swift @@ -14,10 +14,9 @@ struct ActiveCOBView: View { var body: some View { VStack { - Text("Active Carbohydrates").font(.previewHeadline).padding(.top, 20) - cobView().frame(maxHeight: 130).padding(.vertical, 10).padding(.horizontal, 20) - .padding(.bottom, 10) - }.dynamicTypeSize(...DynamicTypeSize.medium) + Text("Active Carbohydrates").font(.previewHeadline).padding(.top, 20).padding(.bottom, 15) + cobView().frame(maxHeight: 130).padding(.bottom, 10).padding(.horizontal, 20) + }.dynamicTypeSize(...DynamicTypeSize.xLarge) } @ViewBuilder private func cobView() -> some View { @@ -27,25 +26,25 @@ struct ActiveCOBView: View { AreaMark( x: .value("Time", $0.date), y: .value("COB", $0.cob) - ).foregroundStyle(Color(.loopYellow)) - } - .chartYAxis { - AxisMarks(values: .automatic(desiredCount: 3)) - } - .chartXAxis { - AxisMarks(values: .stride(by: .hour, count: 2)) { _ in - AxisValueLabel( - format: .dateTime.hour(.defaultDigits(amPM: .omitted)) - .locale(Locale(identifier: "sv")) // Force 24h - ) - AxisGridLine() + ).foregroundStyle(Color(.loopYellow).gradient).opacity(0.8) } + .chartYAxis { + AxisMarks(values: .automatic(desiredCount: 3)) } - } - .chartYScale( - domain: 0 ... maximum - ) - .chartXScale( - domain: Date.now.addingTimeInterval(-1.days.timeInterval) ... Date.now - ) + .chartXAxis { + AxisMarks(values: .stride(by: .hour, count: 2)) { _ in + AxisValueLabel( + format: .dateTime.hour(.defaultDigits(amPM: .omitted)) + .locale(Locale(identifier: "sv")) // 24h format + ) + AxisGridLine() + } + } + .chartYScale( + domain: 0 ... maximum + ) + .chartXScale( + domain: Date.now.addingTimeInterval(-1.days.timeInterval) ... Date.now + ) + .chartLegend(.hidden) } } diff --git a/FreeAPS/Sources/Modules/Home/View/Previews/ActiveIOBView.swift b/FreeAPS/Sources/Modules/Home/View/Previews/ActiveIOBView.swift index 892d15b6c6..8ed713c4f1 100644 --- a/FreeAPS/Sources/Modules/Home/View/Previews/ActiveIOBView.swift +++ b/FreeAPS/Sources/Modules/Home/View/Previews/ActiveIOBView.swift @@ -29,23 +29,69 @@ struct ActiveIOBView: View { var body: some View { VStack { - Text("Active Insulin").font(.previewHeadline).padding(.top, 20) + Text("Active Insulin").font(.previewHeadline).padding(.top, 20).padding(.bottom, 15) iobView().frame(maxHeight: 130).padding(.horizontal, 20) - sumView().frame(maxHeight: 250).padding(.vertical, 30) + sumView().frame(maxHeight: 250).padding(.top, 20).padding(.bottom, 10) }.dynamicTypeSize(...DynamicTypeSize.xLarge) } @ViewBuilder private func iobView() -> some View { - let minimum = data.map(\.iob).min() ?? 0 + // Data + let negIOBData = negIOBdata(data) + // Domain + let minimum = min(data.map(\.iob).min() ?? 0, negIOBData.map(\.iob).min() ?? 0) let minimumRange = min(0, minimum * 1.3) let maximum = (data.map(\.iob).max() ?? 0) * 1.1 - Chart(data) { - AreaMark( - x: .value("Time", $0.date), - y: .value("IOB", $0.iob) - ).foregroundStyle(Color(.insulin)) + Chart { + ForEach(data) { item in + LineMark( + x: .value("Time", item.date), + y: .value("IOB", item.iob) + ).foregroundStyle(by: .value("Time", "Line IOB > 0")) + .lineStyle(StrokeStyle(lineWidth: 0.8)) + + AreaMark( + x: .value("Time", item.date), + y: .value("IOB", item.iob) + ).foregroundStyle(by: .value("Time", "IOB > 0")) + } + ForEach(negIOBData) { item in + AreaMark( + x: .value("Time", item.date), + yStart: .value("IOB", 0), + yEnd: .value("IOB", item.iob) + ).foregroundStyle(by: .value("Time", "IOB < 0")) + } } + .chartForegroundStyleScale( + [ + "IOB > 0": LinearGradient( + gradient: Gradient(colors: [ + Color.insulin.opacity(1), + Color.insulin.opacity(0.4) + ]), + startPoint: .top, + endPoint: .bottom + ), + "IOB < 0": LinearGradient( + gradient: Gradient(colors: [ + Color.red.opacity(1), + Color.red.opacity(1) + ]), + startPoint: .bottom, + endPoint: .top + ), + "Line IOB > 0": LinearGradient( + gradient: Gradient(colors: [ + Color.insulin.opacity(1), + Color.insulin.opacity(1) + ]), + startPoint: .top, + endPoint: .bottom + ) + ] + ) .chartXAxis { AxisMarks(values: .stride(by: .hour, count: 2)) { _ in AxisValueLabel( @@ -64,6 +110,7 @@ struct ActiveIOBView: View { .chartXScale( domain: Date.now.addingTimeInterval(-1.days.timeInterval) ... Date.now ) + .chartLegend(.hidden) } @ViewBuilder private func sumView() -> some View { @@ -93,7 +140,7 @@ struct ActiveIOBView: View { color: Color(.clear) ), BolusSummary( - variable: NSLocalizedString("Average Insulin past 24h", comment: ""), + variable: NSLocalizedString("Average Insulin 10 days", comment: ""), formula: NSLocalizedString(" U", comment: ""), insulin: tddActualAverage, color: .secondary @@ -158,4 +205,21 @@ struct ActiveIOBView: View { } return data } + + private func negIOBdata(_ data: [IOBData]) -> [IOBData] { + var array = [IOBData]() + var previous = data.first + for item in data { + if item.iob < 0 { + if previous?.iob ?? 0 >= 0 { + array.append(IOBData(date: previous?.date ?? .distantPast, iob: 0, cob: 0)) + } + array.append(IOBData(date: item.date, iob: item.iob, cob: 0)) + } else if previous?.iob ?? 0 < 0 { + array.append(IOBData(date: item.date, iob: 0, cob: 0)) + } + previous = item + } + return array + } } diff --git a/FreeAPS/Sources/Modules/Home/View/Previews/LoopsView.swift b/FreeAPS/Sources/Modules/Home/View/Previews/LoopsView.swift index efa6629a4f..c87dc8bb6e 100644 --- a/FreeAPS/Sources/Modules/Home/View/Previews/LoopsView.swift +++ b/FreeAPS/Sources/Modules/Home/View/Previews/LoopsView.swift @@ -12,7 +12,7 @@ struct LoopsView: View { let readings = loopStatistics.1 let percentage = loopStatistics.2 - Text(NSLocalizedString("Loops", comment: "") + " / " + NSLocalizedString("Readings", comment: "")) + Text(NSLocalizedString("Loops", comment: "")) .padding(.bottom, 10).font(.previewHeadline) loopChart(percentage: percentage) @@ -32,20 +32,38 @@ struct LoopsView: View { Text("\(loops)") }.font(.loopFont) } - .padding(.top, 20) - .padding(.bottom, 15) .dynamicTypeSize(...DynamicTypeSize.xLarge) } func loopChart(percentage: Double) -> some View { VStack { Chart { + // Background chart 100 % + if percentage < 100 { + BarMark( + xStart: .value("LoopPercentage", percentage - 4), + xEnd: .value("Full Bar", 100) + ) + .foregroundStyle( + Color(.gray).opacity(0.3) + ) + .clipShape( + UnevenRoundedRectangle( + topLeadingRadius: 0, + bottomLeadingRadius: 0, + bottomTrailingRadius: 4, + topTrailingRadius: 4 + ) + ) + } + + // Loops per readings chart BarMark( x: .value("LoopPercentage", percentage) ) .foregroundStyle( percentage >= 90 ? Color(.darkGreen) : percentage >= 75 ? .orange : .red - ) + ).opacity(1) .clipShape( UnevenRoundedRectangle( topLeadingRadius: 4, diff --git a/FreeAPS/Sources/Modules/Home/View/Previews/TIRView.swift b/FreeAPS/Sources/Modules/Home/View/Previews/TIRView.swift index af2ddeb8d4..6078bb9e95 100644 --- a/FreeAPS/Sources/Modules/Home/View/Previews/TIRView.swift +++ b/FreeAPS/Sources/Modules/Home/View/Previews/TIRView.swift @@ -17,10 +17,134 @@ struct PreviewChart: View { } var body: some View { - let fetched = previewTir() + VStack { + let padding: CGFloat = 40 + // Prepare the chart data + let data = prepareData() + HStack { + Text("Today") + }.padding(.bottom, 15).font(.previewHeadline) + + HStack { + Chart(data) { item in + BarMark( + x: .value("TIR", item.type), + y: .value("Percentage", item.percentage), + width: .fixed(65) + ) + .foregroundStyle(by: .value("Group", item.group)) + .clipShape( + UnevenRoundedRectangle( + topLeadingRadius: (item.last || item.percentage == 100) ? 4 : 0, + bottomLeadingRadius: (item.first || item.percentage == 100) ? 4 : 0, + bottomTrailingRadius: (item.first || item.percentage == 100) ? 4 : 0, + topTrailingRadius: (item.last || item.percentage == 100) ? 4 : 0 + ) + ) + } + .chartForegroundStyleScale([ + NSLocalizedString( + "Low", + comment: "" + ): .red, + NSLocalizedString("In Range", comment: ""): .darkGreen, + NSLocalizedString( + "High", + comment: "" + ): .yellow, + NSLocalizedString( + "Very High", + comment: "" + ): .red, + NSLocalizedString( + "Very Low", + comment: "" + ): .darkRed, + "Separator": colorScheme == .dark ? .black : .white + ]) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .chartLegend(.hidden) + .padding(.bottom, 15) + .padding(.leading, padding) + .frame(maxWidth: (UIScreen.main.bounds.width / 5) + padding) + + sumView(data).offset(x: 0, y: -7) + } + + }.frame(maxHeight: 180) + .padding(.top, 20) + .dynamicTypeSize(...DynamicTypeSize.xLarge) + } + + @ViewBuilder private func sumView(_ data: [TIRinPercent]) -> some View { + let entries = data.reversed().filter { $0.group != "Separator" } + let padding: CGFloat = entries.count == 5 ? 4 : 35 / CGFloat(entries.count) + Grid { + ForEach(entries) { entry in + if entry.group != "Separator" { + GridRow(alignment: .firstTextBaseline) { + if entry.percentage != 0 { + HStack { + Text((tirFormatter.string(for: entry.percentage) ?? "") + "%") + Text(entry.group) + }.font( + entry.group == NSLocalizedString("In Range", comment: "") ? .previewHeadline : .previewSmall + ) + .foregroundStyle( + entry + .group == NSLocalizedString("In Range", comment: "") ? .primary : .secondary + ) + .padding( + .bottom, + (entries.count > 1 && entry.group != entries[entries.count - 1].group) ? padding : 0 + ) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + } + .dynamicTypeSize(...DynamicTypeSize.medium) + } + + private func previewTir() -> [(double: Double, string: String)] { + let hypoLimit = Int(lowLimit) + let hyperLimit = Int(highLimit) + let glucose = readings + let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) }) + let totalReadings = justGlucoseArray.count + let hyperArray = glucose.filter({ $0.glucose >= hyperLimit }) + let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count + var hyperPercentage = round(Double(hyperReadings) / Double(totalReadings) * 100) + let hypoArray = glucose.filter({ $0.glucose <= hypoLimit }) + let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count + var hypoPercentage = round(Double(hypoReadings) / Double(totalReadings) * 100) + let veryHighArray = glucose.filter({ $0.glucose > 197 }) + let veryHighReadings = veryHighArray.compactMap({ each in each.glucose as Int16 }).count + let veryHighPercentage = round(Double(veryHighReadings) / Double(totalReadings) * 100) + let veryLowArray = glucose.filter({ $0.glucose < 60 }) + let veryLowReadings = veryLowArray.compactMap({ each in each.glucose as Int16 }).count + let veryLowPercentage = round(Double(veryLowReadings) / Double(totalReadings) * 100) - let separator: Decimal = 3 + hypoPercentage -= veryLowPercentage + hyperPercentage -= veryHighPercentage + + let tir = round(100 - (hypoPercentage + hyperPercentage + veryHighPercentage + veryLowPercentage)) + var array: [(double: Double, string: String)] = [] + array.append((double: hypoPercentage, string: "Low")) + array.append((double: tir, string: "NormaL")) + array.append((double: hyperPercentage, string: "High")) + array.append((double: veryHighPercentage, string: "Very High")) + array.append((double: veryLowPercentage, string: "Very Low")) + + return array + } + + private func prepareData() -> [TIRinPercent] { + let fetched = previewTir() + let separator: Double = 2 var data: [TIRinPercent] = [ TIRinPercent( type: "TIR", @@ -28,7 +152,7 @@ struct PreviewChart: View { "Very Low", comment: "" ), - percentage: fetched[4].decimal, + percentage: fetched[4].double, id: UUID(), offset: -5, first: true, @@ -49,7 +173,7 @@ struct PreviewChart: View { "Low", comment: "" ), - percentage: fetched[0].decimal, + percentage: fetched[0].double, id: UUID(), offset: -10, first: false, @@ -67,7 +191,7 @@ struct PreviewChart: View { TIRinPercent( type: "TIR", group: NSLocalizedString("In Range", comment: ""), - percentage: fetched[1].decimal, + percentage: fetched[1].double, id: UUID(), offset: 0, first: false, @@ -88,7 +212,7 @@ struct PreviewChart: View { "High", comment: "" ), - percentage: fetched[2].decimal, + percentage: fetched[2].double, id: UUID(), offset: 10, first: false, @@ -109,7 +233,7 @@ struct PreviewChart: View { "Very High", comment: "" ), - percentage: fetched[3].decimal, + percentage: fetched[3].double, id: UUID(), offset: 5, first: false, @@ -117,175 +241,6 @@ struct PreviewChart: View { ) ] - // Preapre the data array - data = prepareData(data_: data) - - return VStack { - Text("Time In Range").padding(.bottom, 10).font(.previewHeadline) - - Chart(data) { item in - BarMark( - x: .value("TIR", item.type), - y: .value("Percentage", item.percentage), - width: .fixed(60) - ) - .foregroundStyle(by: .value("Group", item.group)) - .clipShape( - UnevenRoundedRectangle( - topLeadingRadius: (item.last || item.percentage == 100) ? 4 : 0, - bottomLeadingRadius: (item.first || item.percentage == 100) ? 4 : 0, - bottomTrailingRadius: (item.first || item.percentage == 100) ? 4 : 0, - topTrailingRadius: (item.last || item.percentage == 100) ? 4 : 0 - ) - ) - .annotation(position: .trailing) { - if item.group == NSLocalizedString("In Range", comment: ""), item.percentage > 0 { - HStack { - if item.percentage < 1 { - Text("< 1%") - } else { - Text((tirFormatter.string(from: item.percentage as NSNumber) ?? "") + "%") - } - Text(item.group) - }.font(.previewNormal) - .padding(.leading, 10) - } else if item.group == NSLocalizedString( - "Low", - comment: "" - ), item.percentage > 0.0 { - HStack { - if item.percentage < 1 { - Text("< 1%") - } else { - Text((tirFormatter.string(from: item.percentage as NSNumber) ?? "") + "%") - } - Text(item.group) - } - .offset(x: 0, y: item.offset) - .font(.loopFont) - .padding(.leading, 10) - } else if item.group == NSLocalizedString( - "High", - comment: "" - ), item.percentage > 0 { - HStack { - if item.percentage < 1 { - Text("< 1%") - } else { - Text((tirFormatter.string(from: item.percentage as NSNumber) ?? "") + "%") - } - Text(item.group) - } - .offset(x: 0, y: item.offset) - .font(.loopFont) - .padding(.leading, 10) - } else if item.group == NSLocalizedString( - "Very High", - comment: "" - ), item.percentage > 0 { - HStack { - if item.percentage < 1 { - Text("< 1%") - } else { - Text((tirFormatter.string(from: item.percentage as NSNumber) ?? "") + "%") - } - Text(item.group) - } - .offset(x: 0, y: item.offset) - .font(.loopFont) - .padding(.leading, 10) - } else if item.group == NSLocalizedString( - "Very Low", - comment: "" - ), item.percentage > 0 { - HStack { - if item.percentage < 1 { - Text("< 1%") - } else { - Text((tirFormatter.string(from: item.percentage as NSNumber) ?? "") + "%") - } - Text(item.group) - } - .offset(x: 0, y: item.offset) - .font(.loopFont) - .padding(.leading, 10) - } - } - } - .chartForegroundStyleScale([ - NSLocalizedString( - "Low", - comment: "" - ): .red, - NSLocalizedString("In Range", comment: ""): .darkGreen, - NSLocalizedString( - "High", - comment: "" - ): .yellow, - NSLocalizedString( - "Very High", - comment: "" - ): .red, - NSLocalizedString( - "Very Low", - comment: "" - ): .darkRed, - "Separator": colorScheme == .dark ? .black : .white - ]) - .chartXAxis(.hidden) - .chartYAxis(.hidden) - .chartLegend(.hidden) - .padding(.bottom, 15) - .frame(maxWidth: UIScreen.main.bounds.width / 5) - .offset(x: -UIScreen.main.bounds.width / 5, y: 0) - }.frame(maxHeight: 200) - .padding(.top, 20) - .dynamicTypeSize(...DynamicTypeSize.xLarge) - } - - private func previewTir() -> [(decimal: Decimal, string: String)] { - let hypoLimit = Int(lowLimit) - let hyperLimit = Int(highLimit) - - let glucose = readings - - let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) }) - let totalReadings = justGlucoseArray.count - - let hyperArray = glucose.filter({ $0.glucose >= hyperLimit }) - let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count - var hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100 - - let hypoArray = glucose.filter({ $0.glucose <= hypoLimit }) - let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count - var hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100 - - let veryHighArray = glucose.filter({ $0.glucose > 197 }) - let veryHighReadings = veryHighArray.compactMap({ each in each.glucose as Int16 }).count - let veryHighPercentage = Double(veryHighReadings) / Double(totalReadings) * 100 - - let veryLowArray = glucose.filter({ $0.glucose < 60 }) - let veryLowReadings = veryLowArray.compactMap({ each in each.glucose as Int16 }).count - let veryLowPercentage = Double(veryLowReadings) / Double(totalReadings) * 100 - - hypoPercentage -= veryLowPercentage - hyperPercentage -= veryHighPercentage - - let tir = 100 - (hypoPercentage + hyperPercentage + veryHighPercentage + veryLowPercentage) - - var array: [(decimal: Decimal, string: String)] = [] - array.append((decimal: Decimal(hypoPercentage), string: "Low")) - array.append((decimal: Decimal(tir), string: "NormaL")) - array.append((decimal: Decimal(hyperPercentage), string: "High")) - array.append((decimal: Decimal(veryHighPercentage), string: "Very High")) - array.append((decimal: Decimal(veryLowPercentage), string: "Very Low")) - - return array - } - - private func prepareData(data_: [TIRinPercent]) -> [TIRinPercent] { - var data = data_ - // Remove separators when needed for index in 0 ..< data.count - 2 { if index < data.count - 1 { @@ -333,7 +288,7 @@ struct PreviewChart: View { struct TIRinPercent: Identifiable { let type: String let group: String - let percentage: Decimal + let percentage: Double let id: UUID let offset: CGFloat var first: Bool From b4c935ca9a4679a473f2341d623644175e2fd4c8 Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Mon, 26 Aug 2024 01:07:42 +0200 Subject: [PATCH 10/27] fix merge --- FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json index 4533f9949f..9558a71044 100644 --- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json +++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json @@ -23,6 +23,7 @@ "uploadStats": true, "useAutotune": false, "useCalendar": false, + "displayDelta": false, "debugOptions": false, "glucoseBadge": true, "timeSettings": true, @@ -65,5 +66,4 @@ "individualAdjustmentFactor": 0.5, "displayFatAndProteinOnWatch": true, "addSourceInfoToGlucoseNotifications": false - "displayDelta": false } From 455b38f828d201c59081a4d691f3d5d0674b8fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:04:25 +0200 Subject: [PATCH 11/27] Merge fix --- FreeAPS/Sources/Modules/Home/HomeStateModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index 39a0e08246..69a028f295 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -92,7 +92,7 @@ extension Home { @Published var tddActualAverage: Decimal = 0 @Published var skipGlucoseChart: Bool = false @Published var openAPSSettings: Preferences? - @Published var displayDelta: Bool = + @Published var displayDelta: Bool = true let coredataContext = CoreDataStack.shared.persistentContainer.viewContext From 5c778299e4347c8013b86ded818fb2b019ff4daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Fri, 26 Jul 2024 21:27:08 +0200 Subject: [PATCH 12/27] Configuration profiles --- .../Core_Data.xcdatamodel/contents | 2 +- FreeAPS.xcodeproj/project.pbxproj | 102 +- .../xcshareddata/xcschemes/FreeAPS X.xcscheme | 5 + .../xcshareddata/swiftpm/Package.resolved | 12 +- .../Resources/javascript/prepare/profile.js | 4 - .../defaults/freeaps/freeaps_settings.json | 126 +- .../Resources/json/defaults/preferences.json | 101 +- FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift | 19 +- .../Sources/APS/OpenAPS/TotalDailyDose.swift | 2 +- FreeAPS/Sources/Helpers/Token.swift | 14 + FreeAPS/Sources/Logger/Logger.swift | 5 +- FreeAPS/Sources/Models/Configs.swift | 2 + FreeAPS/Sources/Models/FreeAPSSettings.swift | 3 +- FreeAPS/Sources/Models/Preferences.swift | 2 +- .../BolusCalculatorStateModel.swift | 26 +- .../View/BolusCalculatorConfigRootView.swift | 5 +- .../Sources/Modules/Home/HomeStateModel.swift | 4 + .../NightscoutConfigStateModel.swift | 4 +- .../View/NotificationsConfigRootView.swift | 4 +- .../ProfilePicker/ProfilePickerDataFlow.swift | 5 + .../ProfilePicker/ProfilePickerProvider.swift | 6 + .../ProfilePickerStateModel.swift | 46 + .../View/ProfilePickerRootView.swift | 172 +++ .../Modules/Restore/RestoreDataFlow.swift | 5 + .../Modules/Restore/RestoreProvider.swift | 6 + .../Modules/Restore/RestoreStateModel.swift | 92 ++ .../Restore/View/RestoreRootView.swift | 1073 +++++++++++++++++ .../Modules/Settings/SettingsStateModel.swift | 12 +- .../Settings/View/SettingsRootView.swift | 56 +- FreeAPS/Sources/Router/Screen.swift | 6 + .../Services/Network/NightscoutAPI.swift | 44 - FreeAPS/Sources/Views/TagCloudView.swift | 4 +- 32 files changed, 1760 insertions(+), 209 deletions(-) create mode 100644 FreeAPS/Sources/Helpers/Token.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift create mode 100644 FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreProvider.swift create mode 100644 FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift create mode 100644 FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift diff --git a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents index 8a0bf22421..8dc370d98b 100644 --- a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents +++ b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index aa523958d5..49abe64a4c 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -21,8 +21,14 @@ 190F8CF72BC6F70800EDB473 /* IllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190F8CF62BC6F70800EDB473 /* IllustrationView.swift */; }; 191A9D162BED00A500028D48 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191A9D152BED00A500028D48 /* Version.swift */; }; 191A9D182BED24B000028D48 /* ActiveIOBView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191A9D172BED24B000028D48 /* ActiveIOBView.swift */; }; + 191DF1502C3C113F003E36F6 /* RestoreDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */; }; + 191DF1522C3C1152003E36F6 /* RestoreProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */; }; + 191DF1542C3C116E003E36F6 /* RestoreStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */; }; + 191DF1562C3C1185003E36F6 /* RestoreRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */; }; 1920BF5D2B9DF53200E861FE /* BolusShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1920BF5C2B9DF53200E861FE /* BolusShortcut.swift */; }; 19229B962AFBB84800CD91CA /* Predictions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19229B952AFBB84800CD91CA /* Predictions.swift */; }; + 1922ACBD2C30B25300B28CF3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1922ACBC2C30B25300B28CF3 /* Database.swift */; }; + 192365422C4FE6EB0038AFC4 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192365412C4FE6EB0038AFC4 /* Token.swift */; }; 192424CB2B7A64E70063CBF0 /* NIghtscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */; }; 1924F72C2BA35AE5006644EE /* TotalDailyDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1924F72B2BA35AE5006644EE /* TotalDailyDose.swift */; }; 1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; }; @@ -30,10 +36,13 @@ 1935364028496F7D001E0B16 /* Dynamic structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Dynamic structs.swift */; }; 193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; }; 194297512B815938006B8A0B /* OverridesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194297502B815938006B8A0B /* OverridesView.swift */; }; - 19493A3B2C5997AD00EC83A7 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19493A3A2C5997AD00EC83A7 /* Database.swift */; }; - 19493A3D2C59987700EC83A7 /* DatabaseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19493A3C2C59987700EC83A7 /* DatabaseModels.swift */; }; + 19462AA62C396436009AA396 /* ProfilePickerDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */; }; + 19462AA82C39644E009AA396 /* ProfilePickerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */; }; + 19462AAA2C396463009AA396 /* ProfilePickerStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */; }; + 19462AAC2C39647D009AA396 /* ProfilePickerRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */; }; 194C32772B93A9BF0016FB2A /* OverrideShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C32762B93A9BF0016FB2A /* OverrideShortcuts.swift */; }; 194D7E6E2B974F9F007A38C1 /* LoopsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */; }; + 1955B1EA2C344E950054B0DA /* DatabaseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1955B1E92C344E950054B0DA /* DatabaseModels.swift */; }; 1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */; }; 195D80B42AF6973A00D25097 /* DynamicRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B32AF6973A00D25097 /* DynamicRootView.swift */; }; 195D80B72AF697B800D25097 /* DynamicDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */; }; @@ -571,9 +580,15 @@ 1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; 191A9D152BED00A500028D48 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 191A9D172BED24B000028D48 /* ActiveIOBView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveIOBView.swift; sourceTree = ""; }; + 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreDataFlow.swift; sourceTree = ""; }; + 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreProvider.swift; sourceTree = ""; }; + 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreStateModel.swift; sourceTree = ""; }; + 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreRootView.swift; sourceTree = ""; }; 1920BF5C2B9DF53200E861FE /* BolusShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusShortcut.swift; sourceTree = ""; }; 192202902BAB567800B95BE8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 19229B952AFBB84800CD91CA /* Predictions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predictions.swift; sourceTree = ""; }; + 1922ACBC2C30B25300B28CF3 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; + 192365412C4FE6EB0038AFC4 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIghtscoutExercise.swift; sourceTree = ""; }; 1924F72B2BA35AE5006644EE /* TotalDailyDose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDailyDose.swift; sourceTree = ""; }; 1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -627,10 +642,13 @@ 193F1E3C2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = ""; }; 194297502B815938006B8A0B /* OverridesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridesView.swift; sourceTree = ""; }; - 19493A3A2C5997AD00EC83A7 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; - 19493A3C2C59987700EC83A7 /* DatabaseModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseModels.swift; sourceTree = ""; }; + 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerDataFlow.swift; sourceTree = ""; }; + 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerProvider.swift; sourceTree = ""; }; + 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerStateModel.swift; sourceTree = ""; }; + 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePickerRootView.swift; sourceTree = ""; }; 194C32762B93A9BF0016FB2A /* OverrideShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideShortcuts.swift; sourceTree = ""; }; 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopsView.swift; sourceTree = ""; }; + 1955B1E92C344E950054B0DA /* DatabaseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseModels.swift; sourceTree = ""; }; 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = ""; }; 195D80B32AF6973A00D25097 /* DynamicRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRootView.swift; sourceTree = ""; }; 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDataFlow.swift; sourceTree = ""; }; @@ -1198,6 +1216,25 @@ path = Shortcuts; sourceTree = ""; }; + 191DF14D2C3C110D003E36F6 /* Restore */ = { + isa = PBXGroup; + children = ( + 191DF14F2C3C113F003E36F6 /* RestoreDataFlow.swift */, + 191DF1512C3C1152003E36F6 /* RestoreProvider.swift */, + 191DF1532C3C116E003E36F6 /* RestoreStateModel.swift */, + 191DF14E2C3C1124003E36F6 /* View */, + ); + path = Restore; + sourceTree = ""; + }; + 191DF14E2C3C1124003E36F6 /* View */ = { + isa = PBXGroup; + children = ( + 191DF1552C3C1185003E36F6 /* RestoreRootView.swift */, + ); + path = View; + sourceTree = ""; + }; 1920BF5B2B9DF4B900E861FE /* Bolus */ = { isa = PBXGroup; children = ( @@ -1222,6 +1259,25 @@ name = "Recovered References"; sourceTree = ""; }; + 19462AA32C396413009AA396 /* ProfilePicker */ = { + isa = PBXGroup; + children = ( + 19462AA52C396436009AA396 /* ProfilePickerDataFlow.swift */, + 19462AA72C39644E009AA396 /* ProfilePickerProvider.swift */, + 19462AA92C396463009AA396 /* ProfilePickerStateModel.swift */, + 19462AA42C396422009AA396 /* View */, + ); + path = ProfilePicker; + sourceTree = ""; + }; + 19462AA42C396422009AA396 /* View */ = { + isa = PBXGroup; + children = ( + 19462AAB2C39647D009AA396 /* ProfilePickerRootView.swift */, + ); + path = View; + sourceTree = ""; + }; 194D7E6C2B974EA4007A38C1 /* Previews */ = { isa = PBXGroup; children = ( @@ -1407,28 +1463,24 @@ 3811DE0325C9D31700A708ED /* Modules */ = { isa = PBXGroup; children = ( - F2159A472BA60A0300A0B716 /* ContactTrick */, - 19F191D92BE4F93400F6297E /* Sharing */, - 195D80B22AF696EE00D25097 /* Dynamic */, - 190EBCC229FF134900BA767D /* StatConfig */, - BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */, - 19F95FF129F10F9C00314DDC /* Stat */, - CE94597C29E9E1CD0047C9C6 /* WatchConfig */, - 19E1F7E629D0828B005C8D20 /* IconConfig */, - 19D466A129AA2B0A004D5F33 /* FPUConfig */, - F90692CD274B99850037068D /* HealthKit */, 6DC5D590658EF8B8DF94F9F5 /* AddCarbs */, A9A4C88374496B3C89058A89 /* AddTempTarget */, 672F63EEAE27400625E14BAD /* AutotuneConfig */, A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */, 3811DE0425C9D32E00A708ED /* Base */, C2C98283C436DB934D7E7994 /* Bolus */, + BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */, E8176B120B55CE89F1591542 /* Calibrations */, F75CB57ED6971B46F8756083 /* CGM */, 0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */, + F2159A472BA60A0300A0B716 /* ContactTrick */, E42231DBF0DBE2B4B92D1B15 /* CREditor */, 9E56E3626FAD933385101B76 /* DataTable */, + 195D80B22AF696EE00D25097 /* Dynamic */, + 19D466A129AA2B0A004D5F33 /* FPUConfig */, + F90692CD274B99850037068D /* HealthKit */, 3811DE2725C9D49500A708ED /* Home */, + 19E1F7E629D0828B005C8D20 /* IconConfig */, D8F047E14D567F2B5DBEFD96 /* ISFEditor */, C11D545CED3ECEB525EDEE23 /* LibreConfig */, 3811DE1A25C9D48300A708ED /* Main */, @@ -1437,11 +1489,17 @@ F66B236E00924A05D6A9F9DF /* NotificationsConfig */, 19DC677C29CA66F200FD9EC4 /* OverrideProfilesConfig */, 3E1C41D9301B7058AA7BF5EA /* PreferencesEditor */, + 19462AA32C396413009AA396 /* ProfilePicker */, 99C01B871ACAB3F32CE755C7 /* PumpConfig */, E493126EA71765130F64CCE5 /* PumpSettingsEditor */, + 191DF14D2C3C110D003E36F6 /* Restore */, 3811DE3825C9D4A100A708ED /* Settings */, + 19F191D92BE4F93400F6297E /* Sharing */, 29B478DF61BF8D270F7D8954 /* Snooze */, + 19F95FF129F10F9C00314DDC /* Stat */, + 190EBCC229FF134900BA767D /* StatConfig */, 6517011F19F244F64E1FF14B /* TargetsEditor */, + CE94597C29E9E1CD0047C9C6 /* WatchConfig */, ); path = Modules; sourceTree = ""; @@ -1583,7 +1641,7 @@ 3811DE9725C9D88300A708ED /* NightscoutManager.swift */, 38FE826925CC82DB001FF17A /* NetworkService.swift */, 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */, - 19493A3A2C5997AD00EC83A7 /* Database.swift */, + 1922ACBC2C30B25300B28CF3 /* Database.swift */, ); path = Network; sourceTree = ""; @@ -1860,7 +1918,7 @@ 19A910352A24D6D700C8951B /* Configs.swift */, F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */, 3811DF0125CA9FEA00A708ED /* Credentials.swift */, - 19493A3C2C59987700EC83A7 /* DatabaseModels.swift */, + 1955B1E92C344E950054B0DA /* DatabaseModels.swift */, 1935363F28496F7D001E0B16 /* Dynamic structs.swift */, F270F68C2BAE374C00F6D8DD /* FontTracking.swift */, F2159A512BA60F7A00A0B716 /* FontWeight.swift */, @@ -1913,6 +1971,7 @@ 3811DE5725C9D4D500A708ED /* ProgressBar.swift */, 3811DE5525C9D4D500A708ED /* Publisher.swift */, 38E98A3625F5509500C0CED0 /* String+Extensions.swift */, + 192365412C4FE6EB0038AFC4 /* Token.swift */, 3811DEE325CA063400A708ED /* PropertyWrappers */, E06B9119275B5EEA003C04B6 /* Array+Extension.swift */, CEB434E428B8FF5D00B70274 /* UIColor.swift */, @@ -2967,6 +3026,7 @@ 3811DEB125C9D88300A708ED /* Keychain.swift in Sources */, 382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */, 19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */, + 19462AAA2C396463009AA396 /* ProfilePickerStateModel.swift in Sources */, 194D7E6E2B974F9F007A38C1 /* LoopsView.swift in Sources */, 383948D625CD4D8900E91849 /* FileStorage.swift in Sources */, 3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */, @@ -2988,6 +3048,7 @@ 388E5A5C25B6F0770019842D /* JSON.swift in Sources */, 3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */, 19DC678529CA67A400FD9EC4 /* OverrideProfilesRootView.swift in Sources */, + 19462AA82C39644E009AA396 /* ProfilePickerProvider.swift in Sources */, 389A572026079BAA00BC102F /* Interpolation.swift in Sources */, 19A910382A24EF3200C8951B /* ChartsView.swift in Sources */, 38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */, @@ -3012,6 +3073,7 @@ 3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */, CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */, 38569347270B5DFB0002C50D /* CGMType.swift in Sources */, + 191DF1562C3C1185003E36F6 /* RestoreRootView.swift in Sources */, 3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */, 384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */, 3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */, @@ -3024,6 +3086,7 @@ CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */, CECA4775298DA8310095139F /* DexcomSourceG5.swift in Sources */, 19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */, + 191DF1522C3C1152003E36F6 /* RestoreProvider.swift in Sources */, 3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */, 3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */, 19F191E02BE4F98F00F6297E /* SharingStateModel.swift in Sources */, @@ -3038,6 +3101,7 @@ BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */, CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */, 190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */, + 191DF1502C3C113F003E36F6 /* RestoreDataFlow.swift in Sources */, 3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */, CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */, 38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */, @@ -3090,10 +3154,12 @@ 72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */, E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */, 45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */, + 192365422C4FE6EB0038AFC4 /* Token.swift in Sources */, CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */, D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */, 38E98A3025F52FF700C0CED0 /* Config.swift in Sources */, CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */, + 19462AA62C396436009AA396 /* ProfilePickerDataFlow.swift in Sources */, BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */, 9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */, 19AEF4322B1F5A98006FFE8B /* TIRView.swift in Sources */, @@ -3133,18 +3199,21 @@ FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */, 63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */, CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */, + 1922ACBD2C30B25300B28CF3 /* Database.swift in Sources */, 38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */, CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */, 385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */, F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */, 38887CCE25F5725200944304 /* IOBEntry.swift in Sources */, 38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */, + 1955B1EA2C344E950054B0DA /* DatabaseModels.swift in Sources */, CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */, 195D80BB2AF6980B00D25097 /* DynamicStateModel.swift in Sources */, E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */, 38192E07261BA9960094D973 /* FetchTreatmentsManager.swift in Sources */, 19012CDC291D2CB900FB8210 /* LoopStats.swift in Sources */, 6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */, + 191DF1542C3C116E003E36F6 /* RestoreStateModel.swift in Sources */, DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */, 1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */, F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */, @@ -3193,6 +3262,7 @@ 5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */, BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */, E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */, + 19462AAC2C39647D009AA396 /* ProfilePickerRootView.swift in Sources */, 919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */, F2159A4A2BA60A6000A0B716 /* ContactTrickDataFlow.swift in Sources */, 8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */, diff --git a/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme b/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme index ae75876b19..1f453fa9bb 100644 --- a/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme +++ b/FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme @@ -66,6 +66,11 @@ value = "" isEnabled = "YES"> + + 0, (suggestion.units ?? 0) <= 0 { + let index = reasonString.endIndex + reasonString.insert(contentsOf: " SMBs Disabled.", at: index) + } + // Middleware if targetGlucose != nil, let middlewareString = readMiddleware(json: profile, variable: "mw"), middlewareString.count > 2 @@ -374,12 +387,6 @@ final class OpenAPS { } } - // SMBs Disabled? - if let required = suggestion.insulinReq, required > 0, (suggestion.units ?? 0) <= 0 { - let index = reasonString.endIndex - reasonString.insert(contentsOf: " SMBs Disabled.", at: index) - } - // Save Suggestion to CoreData coredataContext.perform { [self] in if let isf = readReason(reason: reason, variable: "ISF"), diff --git a/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift b/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift index 95c53fa339..e49a715d2d 100644 --- a/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift +++ b/FreeAPS/Sources/APS/OpenAPS/TotalDailyDose.swift @@ -48,7 +48,7 @@ final class TotalDailyDose { } func insulinToday(_ data: [PumpHistoryEvent], increment: Double) -> (bolus: Decimal, basal: Decimal, hours: Double) { - let filtered = data.filter({ $0.timestamp > Calendar.current.startOfDay(for: Date()) }) + let filtered = data.filter({ $0.timestamp >= Calendar.current.startOfDay(for: Date()) }) return totalDailyDose(filtered, increment: increment) } diff --git a/FreeAPS/Sources/Helpers/Token.swift b/FreeAPS/Sources/Helpers/Token.swift new file mode 100644 index 0000000000..cedba82c08 --- /dev/null +++ b/FreeAPS/Sources/Helpers/Token.swift @@ -0,0 +1,14 @@ +import Foundation + +final class Token { + func getIdentifier() -> String { + let keychain = BaseKeychain() + var identfier = keychain.getValue(String.self, forKey: IAPSconfig.id) ?? "" + guard identfier.count > 1 else { + identfier = UUID().uuidString + keychain.setValue(identfier, forKey: IAPSconfig.id) + return identfier + } + return identfier + } +} diff --git a/FreeAPS/Sources/Logger/Logger.swift b/FreeAPS/Sources/Logger/Logger.swift index 9ddaec8273..1b1ae0438d 100644 --- a/FreeAPS/Sources/Logger/Logger.swift +++ b/FreeAPS/Sources/Logger/Logger.swift @@ -112,7 +112,6 @@ final class Logger { static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter) static let apsManager = Logger(category: .apsManager, reporter: baseReporter) static let nightscout = Logger(category: .nightscout, reporter: baseReporter) - static let dynamic = Logger(category: .dynamic, reporter: baseReporter) enum Category: String { case `default` @@ -131,13 +130,13 @@ final class Logger { var logger: Logger { switch self { case .default: return .default - case .service: return .service + case .dynamic, + .service: return .service case .businessLogic: return .businessLogic case .openAPS: return .openAPS case .deviceManager: return .deviceManager case .apsManager: return .apsManager case .nightscout: return .nightscout - case .dynamic: return .dynamic } } diff --git a/FreeAPS/Sources/Models/Configs.swift b/FreeAPS/Sources/Models/Configs.swift index 80b7ab8060..b49f1e4930 100644 --- a/FreeAPS/Sources/Models/Configs.swift +++ b/FreeAPS/Sources/Models/Configs.swift @@ -62,4 +62,6 @@ extension Font { static let carbsDotFont = Font.custom("CarbsDotFont", fixedSize: 12) static let bolusDotFont = Font.custom("BolusDotFont", fixedSize: 12) static let announcementSymbolFont = Font.custom("AnnouncementSymbolFont", fixedSize: 14) + + static let settingsListed = Font.custom("settingsListed", fixedSize: 15) } diff --git a/FreeAPS/Sources/Models/FreeAPSSettings.swift b/FreeAPS/Sources/Models/FreeAPSSettings.swift index 0e9f21f04a..3c1df497e7 100644 --- a/FreeAPS/Sources/Models/FreeAPSSettings.swift +++ b/FreeAPS/Sources/Models/FreeAPSSettings.swift @@ -65,8 +65,7 @@ struct FreeAPSSettings: JSON, Equatable { var disableCGMError: Bool = true var uploadVersion: Bool = true var skipGlucoseChart: Bool = false - var birthDate = Date.distantPast - // var sex: Sex = .secret + var birthDate: Date = .distantPast var sexSetting: Int = 3 var disableHypoTreatment: Bool = false var displayDelta: Bool = false diff --git a/FreeAPS/Sources/Models/Preferences.swift b/FreeAPS/Sources/Models/Preferences.swift index ee4057a3da..0bbe02781f 100644 --- a/FreeAPS/Sources/Models/Preferences.swift +++ b/FreeAPS/Sources/Models/Preferences.swift @@ -67,7 +67,7 @@ extension Preferences { case highTemptargetRaisesSensitivity = "high_temptarget_raises_sensitivity" case lowTemptargetLowersSensitivity = "low_temptarget_lowers_sensitivity" case sensitivityRaisesTarget = "sensitivity_raises_target" - case resistanceLowersTarget + case resistanceLowersTarget = "resistance_lowers_target" case advTargetAdjustments = "adv_target_adjustments" case exerciseMode = "exercise_mode" case halfBasalExerciseTarget = "half_basal_exercise_target" diff --git a/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift b/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift index 3cc9e30492..d1144ee5d0 100644 --- a/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift +++ b/FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift @@ -2,6 +2,8 @@ import SwiftUI extension BolusCalculatorConfig { final class StateModel: BaseStateModel { + @Injected() private var storage: FileStorage! + @Published var overrideFactor: Decimal = 0 @Published var useCalc: Bool = true @Published var fattyMeals: Bool = false @@ -17,9 +19,7 @@ extension BolusCalculatorConfig { subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: { let value = max(min($0, 2), 0.1) overrideFactor = value - }, map: { - $0 - }) + }, map: { $0 }) subscribeSetting(\.allowBolusShortcut, on: $allowBolusShortcut) { allowBolusShortcut = $0 } subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 } subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 } @@ -45,6 +45,26 @@ extension BolusCalculatorConfig { }, map: { $0 }) + + // broadcaster.register(SettingsObserver.self, observer: self) + } + + // Temporary while testing. Replace/remove later + func checkProfileChange() { + guard let settings_ = storage.retrieveRaw(OpenAPS.FreeAPS.settings) else { return } + if let settings = FreeAPSSettings(from: settings_) { + overrideFactor = settings.overrideFactor + print("Override Factor: \(overrideFactor)") + useCalc = settings.useCalc + fattyMeals = settings.fattyMeals + fattyMealFactor = settings.fattyMealFactor + insulinReqPercentage = settings.insulinReqPercentage + displayPredictions = settings.displayPredictions + allowBolusShortcut = settings.allowBolusShortcut + allowedRemoteBolusAmount = settings.allowedRemoteBolusAmount + eventualBG = settings.eventualBG + minumimPrediction = settings.minumimPrediction + } } } } diff --git a/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift b/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift index 6e4d126b02..9c8160ea87 100644 --- a/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift +++ b/FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift @@ -125,7 +125,10 @@ extension BolusCalculatorConfig { } header: { Text("iOS Shortcuts") } } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + state.checkProfileChange() + } .navigationBarTitle("Bolus Calculator") .navigationBarTitleDisplayMode(.automatic) .blur(radius: isPresented ? 5 : 0) diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index e05837b441..08f8fe118d 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -548,6 +548,10 @@ extension Home { let ratio = min(c / (target + c - 100), maxValue) return (ratio * 100) } + + func getIdentifier() -> String { + Token().getIdentifier() + } } } diff --git a/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift b/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift index d1e26b52b6..0ecff87e8a 100644 --- a/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift +++ b/FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift @@ -261,7 +261,7 @@ extension NightscoutConfig { let syncValues = basals.map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) } - // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit) + // SAVE TO STORAGE. SAVE TO PUMP (LoopKit) pump.syncBasalRateSchedule(items: syncValues) { result in switch result { case .success: @@ -272,8 +272,6 @@ extension NightscoutConfig { debug(.service, "Settings have been imported and the Basals saved to pump!") // DIA. Save if changed. let dia = fetchedProfile.dia - print("dia: " + dia.description) - print("pump dia: " + self.dia.description) if dia != self.dia, dia >= 0 { let file = PumpSettings( insulinActionCurve: dia, diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift index 23decc48ea..0423eb8264 100644 --- a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift +++ b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift @@ -100,7 +100,9 @@ extension NotificationsConfig { } } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + } .navigationBarTitle("Notifications") .navigationBarTitleDisplayMode(.automatic) } diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift new file mode 100644 index 0000000000..d2fa344a17 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerDataFlow.swift @@ -0,0 +1,5 @@ +enum ProfilePicker { + enum Config {} +} + +protocol ProfilePickerProvider: Provider {} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift new file mode 100644 index 0000000000..4f696fdd89 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerProvider.swift @@ -0,0 +1,6 @@ +import Combine +import Foundation + +extension ProfilePicker { + final class Provider: BaseProvider, ProfilePickerProvider {} +} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift new file mode 100644 index 0000000000..83e25096c3 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/ProfilePickerStateModel.swift @@ -0,0 +1,46 @@ +import Foundation +import Swinject + +extension ProfilePicker { + final class StateModel: BaseStateModel { + // @Injected() var keychain: Keychain! + + @Published var name: String = "" + @Published var backup: Bool = false + + let coreData = CoreDataStorage() + + func save(_ name_: String) { + coreData.saveProfileSettingName(name: name_) + } + + override func subscribe() { + backup = settingsManager.settings.uploadStats + } + + func getIdentifier() -> String { + Token().getIdentifier() + } + + func activeProfile(_ selectedProfile: String) { + coreData.activeProfile(name: selectedProfile) + } + + func deleteProfileFromDatabase(name: String) { + let database = Database(token: getIdentifier()) + + database.deleteProfile(name) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles \(name) deleted from database") + + case let .failure(error): + debug(.service, "Failed deleting \(name) from database. " + error.localizedDescription) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } +} diff --git a/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift b/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift new file mode 100644 index 0000000000..04f4660f00 --- /dev/null +++ b/FreeAPS/Sources/Modules/ProfilePicker/View/ProfilePickerRootView.swift @@ -0,0 +1,172 @@ +import Combine +import CoreData +import SwiftUI +import Swinject + +extension ProfilePicker { + struct RootView: BaseView { + let resolver: Resolver + @StateObject var state = StateModel() + + @Environment(\.managedObjectContext) var moc + @Environment(\.colorScheme) var colorScheme + + @FetchRequest( + entity: Profiles.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], + predicate: NSPredicate( + format: "name != %@", "" as String + ) + ) var profiles: FetchedResults + + @FetchRequest( + entity: ActiveProfile.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], + predicate: NSPredicate( + format: "active == true" + ) + ) var currentProfile: FetchedResults + + @State var selectedProfile = "" + @State var id = "" + @State var lifetime = Lifetime() + + var body: some View { + Form { + Section { + HStack { + Text("Current profile:").foregroundStyle(.secondary) + Spacer() + if let p = currentProfile.first { + Text(p.name ?? "default") + + if let exists = profiles.first(where: { $0.name == (p.name ?? "default") }), exists.uploaded { + Image(systemName: "cloud") + } + } else { + Text("default") + if profiles.first?.uploaded ?? false { + Image(systemName: "cloud") + } + } + } + } header: { Text("Active settings") } + + footer: { + Text( + "Updates and uploads to the cloud automatically whenever settings are changed and on a daily basis, provided backup is enabled." + ) + } + + Section { + TextField("Name", text: $state.name) + + Button("Save") { + state.save(state.name) + state.activeProfile(state.name) + upload() + }.disabled(state.name.isEmpty) + + } header: { + Text("Save as new profile") + } + + Section { + let uploaded = profiles.filter({ $0.uploaded == true }) + Section { + if profiles.isEmpty { Text("No profiles saved") + } else if profiles.first == uploaded.last, profiles.count == 1 { + Text("No other profiles saved") + } else { + ForEach(uploaded) { profile in + profilesView(for: profile) + .deleteDisabled(profile.name == "default" || profile.name == currentProfile.first?.name ?? "") + } + .onDelete(perform: removeProfile) + } + } + } header: { + HStack { + Text("Load Profile from") + Image(systemName: "cloud").textCase(nil).foregroundStyle(colorScheme == .dark ? .white : .black) + } + } + + Section { + Button("Upload now") { + // If no profiles saved yet + if (profiles.first?.name ?? "NoneXXX") == "NoneXXX" || (profiles.first?.name ?? "default" == "default") { + state.save("default") + state.activeProfile("default") + } + upload() + let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) + impactHeavy.impactOccurred() + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(state.backup ? Color(.systemBlue) : Color(.systemGray4)) + .tint(.white) + + } header: { Text("Backup now") } + + footer: { + if !state.backup { + Text("\nBackup disabled in Sharing settings").foregroundStyle(.orange).bold().textCase(nil) + } + } + } + .dynamicTypeSize(...DynamicTypeSize.xxLarge) + .onAppear { configureView() } + .navigationTitle("Profiles") + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder private func profilesView(for preset: Profiles) -> some View { + if (preset.name ?? "") == (currentProfile.first?.name ?? "BlaBlaXX") { + Text(preset.name ?? "").foregroundStyle(.secondary) + } else { + Text(preset.name ?? "") + .foregroundStyle(.blue) + .navigationLink(to: .restore( + int: 2, + profile: preset.name ?? "", + inSitu: true, + id_: state.getIdentifier(), + uniqueID: state.getIdentifier() + ), from: self) + .onTapGesture { + selectedProfile = preset.name ?? "" + } + } + } + + private func removeProfile(at offsets: IndexSet) { + let database = Database(token: state.getIdentifier()) + for index in offsets { + let profile = profiles[index] + + database.deleteProfile(profile.name ?? "") + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles \(profile.name ?? "") deleted from database") + self.moc.delete(profile) + do { try moc.save() } catch { /* To do: add error */ } + case let .failure(error): + debug( + .service, + "Failed deleting \(profile.name ?? "") from database. " + error.localizedDescription + ) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } + + private func upload() { + let b = BaseNightscoutManager(resolver: resolver) + b.uploadProfileAndSettings(true) + } + } +} diff --git a/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift b/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift new file mode 100644 index 0000000000..7154f89a21 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreDataFlow.swift @@ -0,0 +1,5 @@ +enum Restore { + enum Config {} +} + +protocol RestoreProvider: Provider {} diff --git a/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift b/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift new file mode 100644 index 0000000000..d5e68a8f4a --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreProvider.swift @@ -0,0 +1,6 @@ +import Combine +import Foundation + +extension Restore { + final class Provider: BaseProvider, RestoreProvider {} +} diff --git a/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift b/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift new file mode 100644 index 0000000000..ad55dda713 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/RestoreStateModel.swift @@ -0,0 +1,92 @@ +import Foundation +import SwiftUI +import Swinject + +extension Restore { + final class StateModel: BaseStateModel { + @Published var name: String = "" + @Published var backup: Bool = false + @Published var basalsSaved = false + + /* + @Published var glucoseBadge = false + @Published var glucoseNotificationsAlways = false + @Published var useAlarmSound = false + @Published var addSourceInfoToGlucoseNotifications = false + @Published var lowGlucose: Decimal = 0 + @Published var highGlucose: Decimal = 0 + @Published var carbsRequiredThreshold: Decimal = 0 + @Published var useLiveActivity = false + @Published var units: GlucoseUnits = .mmolL + @Published var closedLoop = false*/ + + let coreData = CoreDataStorage() + let overrrides = OverrideStorage() + let coredataContext = CoreDataStack.shared.persistentContainer.viewContext + + override func subscribe() { + backup = settingsManager.settings.uploadStats + } + + func save(_ name: String) { + coreData.saveProfileSettingName(name: name) + } + + func saveFile(_ file: JSON, filename: String) { + let s = BaseFileStorage() + s.save(file, as: filename) + } + + func activeProfile(_ selectedProfile: String) { + coreData.activeProfile(name: selectedProfile) + } + + func fetchSettingProfileNames() -> [Profiles]? { + coreData.fetchSettingProfileNames() + } + + func saveMealPresets(_ mealPresets: [MigratedMeals]) { + coredataContext.performAndWait { + for item in mealPresets { + let saveToCoreData = Presets(context: self.coredataContext) + saveToCoreData.dish = item.dish + saveToCoreData.carbs = item.carbs as NSDecimalNumber + saveToCoreData.fat = item.fat as NSDecimalNumber + saveToCoreData.protein = item.protein as NSDecimalNumber + } + try? self.coredataContext.save() + } + } + + func saveOverridePresets(_ presets: [MigratedOverridePresets]) { + coredataContext.performAndWait { + for item in presets { + let saveToCoreData = OverridePresets(context: self.coredataContext) + saveToCoreData.percentage = item.percentage + saveToCoreData.target = item.target as NSDecimalNumber + saveToCoreData.end = item.end as NSDecimalNumber + saveToCoreData.start = item.start as NSDecimalNumber + saveToCoreData.id = item.id + saveToCoreData.advancedSettings = item.advancedSettings + saveToCoreData.cr = item.cr + saveToCoreData.duration = item.duration as NSDecimalNumber + saveToCoreData.isf = item.isf + saveToCoreData.name = item.name + saveToCoreData.isfAndCr = item.isndAndCr + saveToCoreData.smbIsAlwaysOff = item.smbAlwaysOff + saveToCoreData.smbIsOff = item.smbIsOff + saveToCoreData.smbMinutes = item.smbMinutes as NSDecimalNumber + saveToCoreData.uamMinutes = item.uamMinutes as NSDecimalNumber + saveToCoreData.date = item.date + saveToCoreData.maxIOB = item.maxIOB as NSDecimalNumber + saveToCoreData.overrideMaxIOB = item.overrideMaxIOB + } + try? self.coredataContext.save() + } + } + + func getIdentifier() -> String { + Token().getIdentifier() + } + } +} diff --git a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift new file mode 100644 index 0000000000..3ad7dce868 --- /dev/null +++ b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift @@ -0,0 +1,1073 @@ +import SwiftUI +import Swinject + +extension Restore { + struct RootView: BaseView { + let resolver: Resolver + @StateObject var state = StateModel() + + let int: Int + let profile: String + let inSitu: Bool + let id_: String + var uniqueID: String + + @Environment(\.dismiss) private var dismiss + + @FetchRequest( + entity: Presets.entity(), sortDescriptors: [] + ) var savedMeals: FetchedResults + + @FetchRequest( + entity: OverridePresets.entity(), sortDescriptors: [] + ) var overrides: FetchedResults + + @State var basals: [BasalProfileEntry]? + @State var basalsOK: Bool = false + @State var basalsSaved: Bool = false + + @State var crs: [CarbRatioEntry]? + @State var crsOK: Bool = false + @State var crsOKSaved: Bool = false + + @State var isfs: [InsulinSensitivityEntry]? + @State var isfsOK: Bool = false + @State var isfsSaved: Bool = false + + @State var settings: Preferences? + @State var settingsOK: Bool = false + @State var settingsSaved: Bool = false + + @State var freeapsSettings: FreeAPSSettings? + @State var freeapsSettingsOK: Bool = false + @State var freeapsSettingsSaved: Bool = false + + @State var profiles: NightscoutProfileStore? + @State var profilesOK: Bool = false + + @State var targets: BGTargetEntry? + @State var targetsOK: Bool = false + @State var targetsSaved: Bool = false + + @State var tempTargets: [TempTarget]? + @State var tempTargetsOK: Bool = false + @State var tempTargetsSaved: Bool = false + + @State var pumpSettings: PumpSettings? + @State var pumpSettingsOK: Bool = false + @State var pumpSettingsSaved: Bool = false + + @State var mealPresets: [MigratedMeals]? + @State var mealPresetsOK: Bool = false + @State var mealPresetsSaved: Bool = false + + @State var overridePresets: [MigratedOverridePresets]? + @State var overridePresetsOK: Bool = false + @State var overridePresetsSaved: Bool = false + + @State var diaOK: Bool = false + @State var diaSaved: Bool = false + + @State var profileList: String? + + @State var page = 0 + @State var token: String = "" + @State var lifetime = Lifetime() + + @State var errorString = "" + + var fetchedVersionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + + var GlucoseFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + return formatter + } + + var body: some View { + Form { + if page == 0 { + onboarding + } else if page == 1 { + tokenView + if token != "" { + startImportView + } + } else if page == 2 { + importedView + } else if page == 3 { + fetchingView + listFetchedView + } else if page == 4 { + savedView + } + } + .navigationTitle(!inSitu ? "Onboarding" : "Switch Configuration") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: Button("Cancel") { + close() + }) + .navigationBarItems(leading: (page > 0 && !inSitu) ? Button("Back") { page -= 1 } : nil) + .onAppear { + page = int + if inSitu { + importSettings(id: id_) + } + } + } + + private var onboarding: some View { + Section { + HStack { + Button { page += 1 } + label: { Text("Yes") } + .buttonStyle(.borderless) + .padding(.leading, 10) + + Spacer() + + Button { + close() + } + label: { Text("No") } + .buttonStyle(.borderless) + .tint(.red) + .padding(.trailing, 10) + } + } header: { + VStack { + Text("Welcome to iAPS, v\(fetchedVersionNumber)!") + .font(.previewHeadline).frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 40) + + Text("Do you have any settings you want to import?\n").font(.previewNormal) + .frame(maxWidth: .infinity, alignment: .center) + } + .textCase(nil) + .foregroundStyle(.primary) + } + footer: { + Text( + "\n\nIf you've previously made any backup of your settings and statistics to the online database you now can choose to import all of these settings to iAPS using your recovery token. The recovery token you can find in your old iAPS app in the Sharing settings.\n\nIf you don't have any settings saved to import make sure to enable the setting \"Share all statistics\" in the Sharing settings later, as this will enable daily auto backups of your current settings and statistics." + ) + .textCase(nil) + .font(.previewNormal) + } + } + + private var tokenView: some View { + Section { + TextField("Token", text: $token) + } + header: { + Text("Enter your recovery token") + .frame(maxWidth: .infinity, alignment: .center) + } + footer: { + Text("\nThe recovery token you can find on your old phone in the Sharing settings.") + .textCase(nil) + .font(.previewNormal) + } + } + + private var startImportView: some View { + Section { + Button { + importSettings(id: token) + page += 1 + } + label: { + Text("Start import").frame(maxWidth: .infinity, alignment: .center) + } + .listRowBackground(!(token == "") ? Color(.systemBlue) : Color(.systemGray4)) + .tint(.white) + } + } + + private func migrateProfiles() { + if !inSitu, (state.fetchSettingProfileNames()?.first?.name ?? "EMPTY_XXX") == "EMPTY_XXX" { + state.activeProfile("default") + changeToken(restoreToken: token) + } + } + + private func importPresets() { + if state.coreData.fetchMealPresets().isEmpty {} + + if state.overrrides.fetchProfiles().isEmpty {} + } + + private var fetchingView: some View { + Section {} header: { + Text( + !noneFetched ? + "\nConfirm the fetched settings before saving" : "No fetched settings" + ) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .center) + .textCase(nil) + .font(.previewHeadline) + } + } + + private var listFetchedView: some View { + Group { + if let profiles = profiles { + if let defaultProfiles = profiles.store["default"] { + // Basals + let basals_ = defaultProfiles.basal.map({ + basal in + BasalProfileEntry( + start: basal.time + ":00", + minutes: offset(basal.time) / 60, + rate: basal.value + ) + }) + + let units: String = freeapsSettings?.units.rawValue ?? GlucoseUnits.mmolL.rawValue + + Section { + ForEach(basals_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.rate.formatted()) + Text("U/h") + } + } + } header: { Text("Basals") } + + // CRs + Section { + let crs_ = defaultProfiles.carbratio.map({ + cr in + CarbRatioEntry(start: cr.time, offset: (cr.timeAsSeconds ?? 0) / 60, ratio: cr.value) + }) + ForEach(crs_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.ratio.formatted()) + Text("g/U") + } + } + } header: { Text("Carb Ratios") } + + // ISFs + Section { + let isfs_ = defaultProfiles.sens.map({ + isf in + InsulinSensitivityEntry( + sensitivity: isf.value, + offset: (isf.timeAsSeconds ?? 0) / 60, + start: isf.time + ) + }) + + ForEach(isfs_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.sensitivity.formatted()) + Text(units + "/U") + } + } + } header: { Text("Insulin Sensitivities") } + + // Targets + Section { + let targets_ = defaultProfiles.target_low.map({ + target in + BGTargetEntry( + low: target.value, + high: target.value, + start: target.time, + offset: (target.timeAsSeconds ?? 0) / 60 + ) + }) + + ForEach(targets_, id: \.start) { item in + HStack { + Text(item.start) + Spacer() + Text(item.low.formatted()) + Text(units) + } + } + } header: { Text("Targets") } + } + } + + // Pump Settings + if let pumpSettings = pumpSettings { + Section { + HStack { + Text("Max Bolus") + Spacer() + Text(pumpSettings.maxBolus.formatted()) + Text("U") + } + HStack { + Text("Max Basal") + Spacer() + Text(pumpSettings.maxBasal.formatted()) + Text("U") + } + HStack { + Text("DIA") + Spacer() + Text(pumpSettings.insulinActionCurve.formatted()) + Text("h") + } + } header: { Text("Pump Settings") } + } + + // Temp Targets + if let tt = tempTargets, tt.isNotEmpty { + let convert: Decimal = (freeapsSettings?.units ?? GlucoseUnits.mmolL) == GlucoseUnits.mmolL ? 0.0555 : 1 + Section { + ForEach(tt, id: \.id) { target in + HStack { + Text(target.name ?? "") + Spacer() + Text("\(target.duration) min") + Spacer() + Text(GlucoseFormatter.string(from: (target.targetBottom ?? 10) * convert as NSNumber) ?? "") + Text(freeapsSettings?.units.rawValue ?? GlucoseUnits.mmolL.rawValue) + } + } + } header: { Text("Temp Targets") } + } + + if let mealPresets = mealPresets, displayCoreData { + // Meal Presets. CoreData + Section { + ForEach(mealPresets, id: \.dish) { preset in + VStack { + Text(preset.dish).foregroundStyle(.secondary) + if preset.carbs > 0 { + HStack { + Text("Carbs") + Spacer() + Text("\(preset.carbs) g") + } + } + if preset.fat > 0 { + HStack { + Text("Fat") + Spacer() + Text("\(preset.fat) g") + } + } + if preset.protein > 0 { + HStack { + Text("Protein ") + Spacer() + Text("\(preset.protein) g") + } + } + } + } + } header: { Text("CoreData Meal Presets") } + } + + if let overridePresets = overridePresets, displayCoreData { + // Override Presets. CoreData + Section { + ForEach(overridePresets, id: \.id) { preset in + HStack { + Text(preset.name) + } + } + } header: { Text("CoreData Override Presets") } + } + + // OpenAPS Settings + if let settings = settings { + Section { + Text(trim(settings.rawJSON.debugDescription)).font(.settingsListed) + } header: { Text("OpenAPS Settings") } + } + + // iAPS Settings + if let freeapsSettings = freeapsSettings { + Section { + Text(trim(freeapsSettings.rawJSON.debugDescription)).font(.settingsListed) + } header: { Text("iAPS Settings") } + } + + Button { + save() + page += 1 + migrateProfiles() + } + label: { Text(inSitu ? "Confirm" : "Save settings") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var importedView: some View { + Group { + Section { + if int == 2 { + HStack { + Text("Profile") + Spacer() + Text(profile) + }.foregroundStyle(.secondary) + } + + if basalsOK { + HStack { + Text("Basals") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if crsOK { + HStack { + Text("Carb Ratios") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if isfsOK { + HStack { + Text("Insulin Sensitivites") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if targetsOK { + HStack { + Text("Targets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if pumpSettingsOK { + HStack { + Text("Pump Settings") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if tempTargetsOK { + HStack { + Text("Temp Targets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if settingsOK { + HStack { + Text("Preferences") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if freeapsSettingsOK { + HStack { + Text("iAPS Settings") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if displayCoreData { + if mealPresetsOK { + HStack { + Text("Meal Presets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + + if overridePresetsOK { + HStack { + Text("Override Presets") + Spacer() + Text("OK") + .foregroundStyle(Color(.darkGreen)) + } + } + } + + } header: { + Text("Fetching settings...").font(.previewNormal) + } + + footer: { + !allDone ? Text("Fetching can take up to a few seconds") : nil + } + + if !allDone { + Section { + Button { + importSettings(id: inSitu ? id_ : token) + let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) + impactHeavy.impactOccurred() + } + label: { Text("Try Again") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } footer: { errorString.isNotEmpty ? Text(errorString).textCase(nil).foregroundStyle(.orange) : nil } + } + + Button { + if noneFetched { + close() + } else { + page += 1 + } + } + label: { Text("Continue") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var savedView: some View { + Group { + Section { + if basalsOK { + HStack { + Text("Basals") + Spacer() + Text(basalsSaved ? "Saved" : "No") + .foregroundStyle(basalsSaved ? Color(.darkGreen) : .red) + } + } + + if crsOK { + HStack { + Text("Carb Ratios") + Spacer() + Text(crsOKSaved ? "Saved" : "No") + .foregroundStyle(crsOKSaved ? Color(.darkGreen) : .red) + } + } + + if isfsOK { + HStack { + Text("Insulin Sensitivites") + Spacer() + Text(isfsSaved ? "Saved" : "No") + .foregroundStyle(isfsSaved ? Color(.darkGreen) : .red) + } + } + + if targetsOK { + HStack { + Text("Targets") + Spacer() + Text(targetsSaved ? "Saved" : "No") + .foregroundStyle(targetsSaved ? Color(.darkGreen) : .red) + } + } + + if pumpSettingsOK { + HStack { + Text("Pump Settings") + Spacer() + Text(pumpSettingsSaved ? "Saved" : "No") + .foregroundStyle(pumpSettingsSaved ? Color(.darkGreen) : .red) + } + } + + if tempTargetsOK { + HStack { + Text("Temp Targets") + Spacer() + Text(tempTargetsSaved ? "Saved" : "No") + .foregroundStyle(tempTargetsSaved ? Color(.darkGreen) : .red) + } + } + + if settingsOK { + HStack { + Text("Preferences") + Spacer() + Text(settingsSaved ? "Saved" : "No") + .foregroundStyle(settingsSaved ? Color(.darkGreen) : .red) + } + } + + if freeapsSettingsOK { + HStack { + Text("iAPS Settings") + Spacer() + Text(freeapsSettingsSaved ? "Saved" : "No") + .foregroundStyle(freeapsSettingsSaved ? Color(.darkGreen) : .red) + } + } + + if displayCoreData { + if mealPresetsOK { + HStack { + Text("Meal Presets") + Spacer() + Text(mealPresetsSaved ? "Saved" : "No") + .foregroundStyle(mealPresetsSaved ? Color(.darkGreen) : .red) + } + } + + if overridePresetsOK { + HStack { + Text("Override Presets") + Spacer() + Text(overridePresetsSaved ? "Saved" : "No") + .foregroundStyle(overridePresetsSaved ? Color(.darkGreen) : .red) + } + } + } + + } header: { + Text("Saving settings...").font(.previewNormal) + } + + Button { close() } + label: { Text("OK") } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color(.systemBlue)) + .tint(.white) + } + } + + private var allDone: Bool { + ( + basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && tempTargetsOK && + mealPresetsOK && overridePresetsOK + ) || + ( + inSitu && basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && + tempTargetsOK + ) + } + + private var noneFetched: Bool { + !basalsOK && !isfsOK && !crsOK && !freeapsSettingsOK && !settingsOK && !targetsOK && !pumpSettingsOK && + !tempTargetsOK && !mealPresetsOK && !overridePresetsOK + } + + private func importSettings(id: String) { + var profile_ = "default" + if inSitu { + profile_ = profile + } else if profile_ == "default" { + // To not overwrite any eventual other current profile with the default settings when force onbarding (or testing) + state.activeProfile("default") + } + + fetchProfiles(token: id, name: profile_) + fetchSettings(token: id, name: profile_) + fetchPreferences(token: id, name: profile_) + fetchPumpSettings(token: id, name: profile_) + fetchTempTargets(token: id, name: profile_) + // CoreData + fetchMealPresets(token: id, name: profile_) + fetchOverridePresets(token: id, name: profile_) + } + + private func addError(_ error: String) { + if errorString.isEmpty { + errorString += error + } + } + + private func close() { + onboardingDone() + if inSitu { + dismiss() + } + } + + private var displayCoreData: Bool { + !inSitu + } + + private func fetchProfiles() { + guard let profiles = profileList else { return } + let string = profiles.components(separatedBy: ",") + for item in string { + CoreDataStorage().migrateProfileSettingName(name: item) + } + } + + func changeToken(restoreToken: String) { + let newToken = state.getIdentifier() + if newToken != restoreToken { + let database = Database(token: newToken) + database.moveProfiles(token: newToken, restoreToken: restoreToken) + .sink { completion in + switch completion { + case .finished: + debug(.service, "List of profiles moved to a new token") + self.retrieveProfiles(restoreToken: newToken) + self.fetchProfiles() + case let .failure(error): + debug(.service, "Failed moving profiles to a new token " + error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: {} + .store(in: &lifetime) + } + } + + func retrieveProfiles(restoreToken: String) { + let database = Database(token: restoreToken) + let coreData = CoreDataStorage() + + database.fetchProfiles() + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "List of profiles fetched from database") + self.fetchProfiles() + if !coreData.checkIfActiveProfile() { + coreData.activeProfile(name: "default") + debug(.service, "default is current profile") + } + case let .failure(error): + debug(.service, "Failed fetching List of profiles from database " + error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { self.profileList = $0.profiles } + .store(in: &lifetime) + } + + private func fetchPreferences(token: String, name: String) { + let database = Database(token: token) + database.fetchPreferences(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Preferences fetched from database. Profile: \(name)") + self.settingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Preferences: " + error.localizedDescription) + } + } + receiveValue: { self.settings = $0 } + .store(in: &lifetime) + } + + private func fetchSettings(token: String, name: String) { + let database = Database(token: token) + database.fetchSettings(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Settings fetched from database. Profile: \(name)") + self.freeapsSettingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("iAPS Settings: " + error.localizedDescription) + } + } + receiveValue: { + self.freeapsSettings = $0 + } + .store(in: &lifetime) + } + + private func fetchProfiles(token: String, name: String) { + let database = Database(token: token) + database.fetchProfile(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Profiles fetched from database. Profile: \(name)") + self.basalsOK = true + self.isfsOK = true + self.crsOK = true + self.targetsOK = true + self.diaOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + errorString += error.localizedDescription + print("Profiles: No") + } + } + receiveValue: { self.profiles = $0 + } + .store(in: &lifetime) + } + + private func fetchPumpSettings(token: String, name: String) { + let database = Database(token: token) + database.fetchPumpSettings(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Pump Settings fetched from database. Profile: \(name)") + self.pumpSettingsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Pump Settings: " + error.localizedDescription) + } + } + receiveValue: { self.pumpSettings = $0 } + .store(in: &lifetime) + } + + private func fetchTempTargets(token: String, name: String) { + let database = Database(token: token) + database.fetchTempTargets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Temp Targets fetched from database. Profile: \(name)") + self.tempTargetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError("Temp Targets: " + error.localizedDescription) + } + } + receiveValue: { + self.tempTargets = $0.tempTargets + } + .store(in: &lifetime) + } + + private func fetchMealPresets(token: String, name: String) { + let database = Database(token: token) + database.fetchMealPressets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Meal Presets fetched from database. Profile: \(name)") + self.mealPresetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { + self.mealPresets = $0.presets + } + .store(in: &lifetime) + } + + private func fetchOverridePresets(token: String, name: String) { + let database = Database(token: token) + database.fetchOverridePressets(name) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Override Presets fetched from database. Profile: \(name)") + self.overridePresetsOK = true + case let .failure(error): + debug(.service, error.localizedDescription) + addError(error.localizedDescription) + } + } + receiveValue: { + self.overridePresets = $0.presets + } + .store(in: &lifetime) + } + + private func verifyProfiles() { + if let fetchedProfiles = profiles { + if let defaultProfiles = fetchedProfiles.store["default"] { + // Basals + let basals_ = defaultProfiles.basal.map({ + basal in + BasalProfileEntry( + start: basal.time + ":00", + minutes: self.offset(basal.time) / 60, + rate: basal.value + ) + }) + + state.saveFile(basals_, filename: OpenAPS.Settings.basalProfile) + debug(.service, "Imported Basals have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + basalsSaved = true + + // Glucoce Unit + let preferredUnit = GlucoseUnits(rawValue: defaultProfiles.units) ?? .mmolL + + // ISFs + let sensitivities = defaultProfiles.sens.map { sensitivity -> InsulinSensitivityEntry in + InsulinSensitivityEntry( + sensitivity: sensitivity.value, + offset: self.offset(sensitivity.time) / 60, + start: sensitivity.time + ) + } + + let isfs_ = InsulinSensitivities( + units: preferredUnit, + userPrefferedUnits: preferredUnit, + sensitivities: sensitivities + ) + + state.saveFile(isfs_, filename: OpenAPS.Settings.insulinSensitivities) + + debug(.service, "Imported ISFs have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + isfsSaved = true + + // CRs + let carbRatios = defaultProfiles.carbratio.map({ + cr -> CarbRatioEntry in + CarbRatioEntry( + start: cr.time, + offset: (cr.timeAsSeconds ?? 0) / 60, + ratio: cr.value + ) + }) + let crs_ = CarbRatios(units: CarbUnit.grams, schedule: carbRatios) + + state.saveFile(crs_, filename: OpenAPS.Settings.carbRatios) + debug(.service, "Imported CRs have been saved to file storage, profile: \(fetchedProfiles.profile ?? "").") + crsOKSaved = true + + // Targets + let glucoseTargets = defaultProfiles.target_low.map({ + target -> BGTargetEntry in + BGTargetEntry( + low: target.value, + high: target.value, + start: target.time, + offset: (target.timeAsSeconds ?? 0) / 60 + ) + }) + let targets_ = BGTargets(units: preferredUnit, userPrefferedUnits: preferredUnit, targets: glucoseTargets) + + state.saveFile(targets_, filename: OpenAPS.Settings.bgTargets) + debug( + .service, + "Imported Targets have been saved to file storage, profile: \(fetchedProfiles.profile ?? "")." + ) + targetsSaved = true + } + } + } + + private func verifySettings() { + if let fetchedSettings = freeapsSettings { + state.saveFile(fetchedSettings, filename: OpenAPS.FreeAPS.settings) + freeapsSettingsSaved = true + debug(.service, "Imported iAPS Settings have been saved to file storage, profile: \(profile).") + } + } + + private func verifyPreferences() { + if let fetchedSettings = settings { + state.saveFile(fetchedSettings, filename: OpenAPS.Settings.preferences) + settingsSaved = true + debug(.service, "Imported Preferences have been saved to file storage, profile: \(profile).") + } + } + + private func verifyPumpSettings() { + if let fetchedSettings = pumpSettings { + state.saveFile(fetchedSettings, filename: OpenAPS.Settings.settings) + pumpSettingsSaved = true + debug(.service, "Imported Pump settings have been saved to file storage, profile: \(profile).") + } + } + + private func verifyTempTargets() { + if let fetchedTargets = tempTargets { + state.saveFile(fetchedTargets, filename: OpenAPS.Settings.tempTargets) + tempTargetsSaved = true + debug(.service, "Imported Temp targets have been saved to file storage, profile: \(profile).") + } + } + + private func verifyMealPresets() { + if let mealPresets = mealPresets, !inSitu, savedMeals.isEmpty { + state.saveMealPresets(mealPresets) + mealPresetsSaved = true + debug(.service, "Imported Meal presets have been saved to CoreData, profile: \(profile).") + } + } + + private func verifyOverridePresets() { + if let overridePresets = overridePresets, !inSitu, overrides.isEmpty { + state.saveOverridePresets(overridePresets) + overridePresetsSaved = true + debug(.service, "Imported Override presets have been saved to CoreData, profile: \(profile).") + } + } + + private func onboardingDone() { + CoreDataStorage().saveOnbarding() + } + + private func offset(_ string: String) -> Int { + let hours = Int(string.prefix(2)) ?? 0 + let minutes = Int(string.suffix(2)) ?? 0 + return ((hours * 60) + minutes) * 60 + } + + private func save() { + verifyProfiles() + verifySettings() + verifyPreferences() + verifyPumpSettings() + verifyTempTargets() + verifyMealPresets() + verifyOverridePresets() + state.activeProfile(profile) + } + + private func trim(_ string: String) -> String { + let trim = string + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\\n", with: "") + .replacingOccurrences(of: "\\", with: "") + .replacingOccurrences(of: "}", with: "") + .replacingOccurrences(of: "{", with: "") + .replacingOccurrences( + of: "\"", + with: "", + options: NSString.CompareOptions.literal, + range: nil + ) + .replacingOccurrences(of: "[", with: "\n") + .replacingOccurrences(of: "]", with: "\n") + let data = trim.components(separatedBy: ",").sorted { $0.count < $1.count } + .debugDescription.replacingOccurrences(of: ", ", with: "\n") + .replacingOccurrences(of: "[", with: "") + .replacingOccurrences(of: "]", with: "") + .replacingOccurrences(of: "\"", with: "") + + return data + } + } +} diff --git a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift index d64c08b070..79e53e12f8 100644 --- a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift +++ b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift @@ -1,3 +1,5 @@ +import Combine +import LoopKit import SwiftUI extension Settings { @@ -5,6 +7,7 @@ extension Settings { @Injected() private var broadcaster: Broadcaster! @Injected() private var fileManager: FileManager! @Injected() private var nightscoutManager: NightscoutManager! + @Injected() private var storage: FileStorage! @Published var closedLoop = false @Published var debugOptions = false @@ -25,7 +28,6 @@ extension Settings { broadcaster.register(SettingsObserver.self, observer: self) buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" // Read branch information from the branch.txt instead of infoDictionary @@ -80,6 +82,14 @@ extension Settings { func deleteOverrides() { nightscoutManager.deleteAllNSoverrrides() // For testing } + + func startOnboarding() { + CoreDataStorage().startOnbarding() + } + + func getIdentifier() -> String { + Token().getIdentifier() + } } } diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index b069218fb6..6d1b99203a 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -15,7 +15,33 @@ extension Settings { ) ) var fetchedVersionNumber: FetchedResults + @FetchRequest( + entity: ActiveProfile.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var currentProfile: FetchedResults + + @FetchRequest( + entity: Onboarding.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var onboarded: FetchedResults + + private var GlucoseFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + return formatter + } + var body: some View { + if onboarded.first?.firstRun ?? true { + Restore.RootView(resolver: resolver, int: 0, profile: "default", inSitu: false, id_: "", uniqueID: "") + + } else { + settingsView + } + } + + var settingsView: some View { Form { Section { Toggle("Closed loop", isOn: $state.closedLoop) @@ -61,6 +87,11 @@ extension Settings { Text("Notifications").navigationLink(to: .notificationsConfig, from: self) } header: { Text("Services") } + Section { + Text("\(currentProfile.first?.name ?? "default")").foregroundStyle(.green).bold() + .navigationLink(to: .profiles, from: self) + } header: { Text("Configuration Profiles") } + Section { Text("Pump Settings").navigationLink(to: .pumpSettingsEditor, from: self) Text("Basal Profile").navigationLink(to: .basalProfileEditor, from: self) @@ -89,7 +120,7 @@ extension Settings { if state.debugOptions { Group { HStack { - Text("NS Upload Profile and Settings") + Text("Upload Profile and Settings") Button("Upload") { state.uploadProfileAndSettings(true) } .frame(maxWidth: .infinity, alignment: .trailing) .buttonStyle(.borderedProminent) @@ -107,6 +138,17 @@ extension Settings { HStack { Toggle("Ignore flat CGM readings", isOn: $state.disableCGMError) } + + // HStack { + Text("Test Onboarding") + .navigationLink(to: .restore( + int: 0, + profile: "default", + inSitu: true, + id_: "", + uniqueID: state.getIdentifier() + ), from: self) + .foregroundStyle(.blue) } Group { Text("Preferences") @@ -148,6 +190,13 @@ extension Settings { .navigationLink(to: .configEditor(file: OpenAPS.Monitor.glucose), from: self) } + Group { + Text("Override Presets uploaded") + .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedOverridePresets), from: self) + Text("Meal Presets uploaded") + .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedMealPresets), from: self) + } + Group { Text("Target presets") .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self) @@ -178,7 +227,10 @@ extension Settings { ShareSheet(activityItems: state.logItems()) } .dynamicTypeSize(...DynamicTypeSize.xxLarge) - .onAppear(perform: configureView) + .onAppear { + configureView() + state.closedLoop = state.settingsManager.settings.closedLoop // Remove later. Test + } .navigationTitle("Settings") .navigationBarItems(trailing: Button("Close", action: state.hideSettingsModal)) .navigationBarTitleDisplayMode(.inline) diff --git a/FreeAPS/Sources/Router/Screen.swift b/FreeAPS/Sources/Router/Screen.swift index 392fd2d572..62d32dd22e 100644 --- a/FreeAPS/Sources/Router/Screen.swift +++ b/FreeAPS/Sources/Router/Screen.swift @@ -37,6 +37,8 @@ enum Screen: Identifiable, Hashable { case dynamicISF case contactTrick case sharing + case profiles + case restore(int: Int, profile: String, inSitu: Bool, id_: String, uniqueID: String) var id: Int { String(reflecting: self).hashValue } } @@ -111,6 +113,10 @@ extension Screen { ContactTrick.RootView(resolver: resolver) case .sharing: Sharing.RootView(resolver: resolver) + case .profiles: + ProfilePicker.RootView(resolver: resolver) + case let .restore(int: int, profile: profile, inSitu: inSitu, id_: id_, uniqueID: uniqueID): + Restore.RootView(resolver: resolver, int: int, profile: profile, inSitu: inSitu, id_: id_, uniqueID: uniqueID) } } diff --git a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift index 926884acfd..8fb61fb608 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift @@ -558,50 +558,6 @@ extension NightscoutAPI { .eraseToAnyPublisher() } - func uploadPrefs(_ prefs: NightscoutPreferences) -> AnyPublisher { - let statURL = IAPSconfig.statURL - var components = URLComponents() - components.scheme = statURL.scheme - components.host = statURL.host - components.port = statURL.port - components.path = Config.sharePath - - var request = URLRequest(url: components.url!) - request.allowsConstrainedNetworkAccess = false - request.timeoutInterval = Config.timeout - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - request.httpBody = try! JSONCoding.encoder.encode(prefs) - request.httpMethod = "POST" - - return service.run(request) - .retry(Config.retryCount) - .map { _ in () } - .eraseToAnyPublisher() - } - - func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher { - let statURL = IAPSconfig.statURL - var components = URLComponents() - components.scheme = statURL.scheme - components.host = statURL.host - components.port = statURL.port - components.path = Config.sharePath - - var request = URLRequest(url: components.url!) - request.allowsConstrainedNetworkAccess = false - request.timeoutInterval = Config.timeout - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - request.httpBody = try! JSONCoding.encoder.encode(settings) - request.httpMethod = "POST" - - return service.run(request) - .retry(Config.retryCount) - .map { _ in () } - .eraseToAnyPublisher() - } - func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher { var components = URLComponents() components.scheme = url.scheme diff --git a/FreeAPS/Sources/Views/TagCloudView.swift b/FreeAPS/Sources/Views/TagCloudView.swift index f10d7edfe6..3c78008bf5 100644 --- a/FreeAPS/Sources/Views/TagCloudView.swift +++ b/FreeAPS/Sources/Views/TagCloudView.swift @@ -58,7 +58,7 @@ struct TagCloudView: View { case textTag where textTag.contains("SMB Delivery Ratio:"): return .uam case textTag where textTag.contains("Bolus"), - textTag where textTag.contains("TDD:"): + textTag where textTag.contains("Insulin 24h:"): return .purple case textTag where textTag.contains("tdd_factor"), textTag where textTag.contains("Sigmoid function"), @@ -72,6 +72,8 @@ struct TagCloudView: View { return .purple case textTag where textTag.contains("Middleware:"): return .red + case textTag where textTag.contains("Configuration:"): + return Color(.darkGreen) default: return .insulin } From f1d2e2b64962bd6b0ddb1d3a2f1132b34204a564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Mon, 29 Jul 2024 18:50:16 +0200 Subject: [PATCH 13/27] Only ask to import reset settings for old iAPS users. Make the tranistion to this new iAPS version more seamless and less "scary". New Views for onboarding and import. Move the code out from Settings module. --- FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift | 2 +- .../Sources/Modules/Home/HomeStateModel.swift | 22 +++++++ .../Modules/Home/View/HomeRootView.swift | 48 +++++++++++++-- .../Restore/View/RestoreRootView.swift | 60 +++++++++++++++++-- .../Modules/Settings/SettingsStateModel.swift | 1 - .../Settings/View/SettingsRootView.swift | 14 ----- 6 files changed, 123 insertions(+), 24 deletions(-) diff --git a/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift b/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift index be4bf429e9..05534c2d91 100644 --- a/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift +++ b/FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift @@ -367,7 +367,7 @@ final class OpenAPS { // Active Configuration profile let active = CoreDataStorage().fetchActiveProfile() if active != "default" { - let index = reasonString.firstIndex(of: ";") ?? reasonString.index(reasonString.startIndex, offsetBy: -1) + let index = reasonString.firstIndex(of: ";") ?? reasonString.index(reasonString.startIndex, offsetBy: 0) reasonString.insert(contentsOf: ", Configuration: \(active)", at: index) } diff --git a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift index 08f8fe118d..8a2b6f0440 100644 --- a/FreeAPS/Sources/Modules/Home/HomeStateModel.swift +++ b/FreeAPS/Sources/Modules/Home/HomeStateModel.swift @@ -92,6 +92,7 @@ extension Home { @Published var tddActualAverage: Decimal = 0 @Published var skipGlucoseChart: Bool = false @Published var displayDelta: Bool = false + @Published var openAPSSettings: Preferences? let coredataContext = CoreDataStack.shared.persistentContainer.viewContext @@ -308,6 +309,27 @@ extension Home { } } + func token() -> String { + Token().getIdentifier() + } + + func fetchPreferences() { + let token = token() + let database = Database(token: token) + database.fetchPreferences("default") + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + debug(.service, "Preferences fetched from database. Profile: default") + case let .failure(error): + debug(.service, error.localizedDescription) + } + } + receiveValue: { self.openAPSSettings = $0 } + .store(in: &lifetime) + } + private func setupGlucose() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index a98ecc3b9a..b2484e928c 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -45,6 +45,11 @@ extension Home { sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] ) var enactedSliderTT: FetchedResults + @FetchRequest( + entity: Onboarding.entity(), + sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] + ) var onboarded: FetchedResults + private var numberFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -692,8 +697,40 @@ extension Home { .background(TimeEllipse(characters: string.count)) } + func onboardingView(token: String) -> some View { + Restore.RootView( + resolver: resolver, + int: 0, + profile: "default", + inSitu: false, + id_: token, + uniqueID: token, + openAPS: nil + ) + } + + func importResetSettingsView(token: String, settings: Preferences) -> some View { + Restore.RootView( + resolver: resolver, + int: -1, + profile: "default", + inSitu: false, + id_: token, + uniqueID: token, + openAPS: settings + ) + } + var body: some View { GeometryReader { geo in + if onboarded.first?.firstRun ?? true { + let token = state.token() + if let openAPSSettings = state.openAPSSettings { + importResetSettingsView(token: token, settings: openAPSSettings) + } else { + onboardingView(token: token) + } + } else { VStack(spacing: 0) { // Header View headerView(geo, extra: (displayGlucose && !state.skipGlucoseChart) ? 59 : 0) @@ -748,12 +785,16 @@ extension Home { .frame(width: 320, height: 60) bolusProgressView(progress: progress, amount: amount) } - .frame(maxWidth: .infinity, alignment: .center) - .offset(x: 0, y: -100) } } + } + } + .onAppear { + configureView() + if onboarded.first?.firstRun ?? true { + state.fetchPreferences() + } } - .onAppear(perform: configureView) .navigationTitle("Home") .navigationBarHidden(true) .ignoresSafeArea(.keyboard) @@ -776,7 +817,6 @@ extension Home { } ) } - .onAppear(perform: configureView) } private var popup: some View { diff --git a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift index 3ad7dce868..0eaf59e5a5 100644 --- a/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift +++ b/FreeAPS/Sources/Modules/Restore/View/RestoreRootView.swift @@ -11,6 +11,7 @@ extension Restore { let inSitu: Bool let id_: String var uniqueID: String + var openAPS: Preferences? @Environment(\.dismiss) private var dismiss @@ -87,7 +88,9 @@ extension Restore { var body: some View { Form { - if page == 0 { + if page == -1 { + importResetSettingsView + } else if page == 0 { onboarding } else if page == 1 { tokenView @@ -111,12 +114,50 @@ extension Restore { .navigationBarItems(leading: (page > 0 && !inSitu) ? Button("Back") { page -= 1 } : nil) .onAppear { page = int - if inSitu { + if inSitu, int != -1 { importSettings(id: id_) } } } + private var importResetSettingsView: some View { + Section { + HStack { + Button { + importOpenAPSOnly() + page = 2 + } + label: { Text("Yes") } + .buttonStyle(.borderless) + .padding(.leading, 10) + + Spacer() + + Button { + close() + } + label: { Text("No") } + .buttonStyle(.borderless) + .tint(.red) + .padding(.trailing, 10) + } + } header: { + VStack { + Text("Welcome to iAPS, v\(fetchedVersionNumber)!") + .font(.previewHeadline).frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 40) + + Text( + "In this new version your OpenAPS settings have been reset to default settings, due to a resolved Type error issue.\n\nFortunately you have a backup of your old OpenAPS settings in the cloud.\n\nDo you want to try to restore these settings now?\n" + ) + .font(.previewNormal) + .frame(maxWidth: .infinity, alignment: .center) + } + .textCase(nil) + .foregroundStyle(.primary) + } + } + private var onboarding: some View { Section { HStack { @@ -513,7 +554,7 @@ extension Restore { } } header: { - Text("Fetching settings...").font(.previewNormal) + Text(!allDone ? "Fetching settings..." : "Settings fetched").font(.previewNormal) } footer: { @@ -644,7 +685,7 @@ extension Restore { } } header: { - Text("Saving settings...").font(.previewNormal) + Text(!allSaved ? "Saving settings..." : "Settings saved").font(.previewNormal) } Button { close() } @@ -664,6 +705,12 @@ extension Restore { inSitu && basalsOK && isfsOK && crsOK && freeapsSettingsOK && settingsOK && targetsOK && pumpSettingsOK && tempTargetsOK ) + || + int == -1 && settingsOK + } + + private var allSaved: Bool { + settingsSaved && int == -1 } private var noneFetched: Bool { @@ -690,6 +737,11 @@ extension Restore { fetchOverridePresets(token: id, name: profile_) } + private func importOpenAPSOnly() { + settings = openAPS + settingsOK = true + } + private func addError(_ error: String) { if errorString.isEmpty { errorString += error diff --git a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift index 79e53e12f8..58720e9102 100644 --- a/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift +++ b/FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift @@ -7,7 +7,6 @@ extension Settings { @Injected() private var broadcaster: Broadcaster! @Injected() private var fileManager: FileManager! @Injected() private var nightscoutManager: NightscoutManager! - @Injected() private var storage: FileStorage! @Published var closedLoop = false @Published var debugOptions = false diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index 6d1b99203a..a622816103 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -20,11 +20,6 @@ extension Settings { sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] ) var currentProfile: FetchedResults - @FetchRequest( - entity: Onboarding.entity(), - sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)] - ) var onboarded: FetchedResults - private var GlucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -33,15 +28,6 @@ extension Settings { } var body: some View { - if onboarded.first?.firstRun ?? true { - Restore.RootView(resolver: resolver, int: 0, profile: "default", inSitu: false, id_: "", uniqueID: "") - - } else { - settingsView - } - } - - var settingsView: some View { Form { Section { Toggle("Closed loop", isOn: $state.closedLoop) From a68a54da53c8e27fefa9c11335215f38e5bfde5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 31 Jul 2024 03:14:57 +0200 Subject: [PATCH 14/27] Check if old/new iAPS user --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index b2484e928c..1f52555e76 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -697,14 +697,14 @@ extension Home { .background(TimeEllipse(characters: string.count)) } - func onboardingView(token: String) -> some View { + func onboardingView() -> some View { Restore.RootView( resolver: resolver, int: 0, profile: "default", inSitu: false, - id_: token, - uniqueID: token, + id_: "", + uniqueID: "", openAPS: nil ) } @@ -725,10 +725,11 @@ extension Home { GeometryReader { geo in if onboarded.first?.firstRun ?? true { let token = state.token() - if let openAPSSettings = state.openAPSSettings { + // If old iAPS user + if let openAPSSettings = state.openAPSSettings, !fetchedPercent.isEmpty, !fetchedProfiles.isEmpty { importResetSettingsView(token: token, settings: openAPSSettings) } else { - onboardingView(token: token) + onboardingView() } } else { VStack(spacing: 0) { From 03e494bc95defdcb440dc9ad939eeb2dcd1924b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 31 Jul 2024 03:48:53 +0200 Subject: [PATCH 15/27] Updates --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 1f52555e76..6387d22b7c 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -725,10 +725,11 @@ extension Home { GeometryReader { geo in if onboarded.first?.firstRun ?? true { let token = state.token() - // If old iAPS user - if let openAPSSettings = state.openAPSSettings, !fetchedPercent.isEmpty, !fetchedProfiles.isEmpty { + // If old iAPS user pre v4.9.0 (not perfect yet) + if state.glucose.isNotEmpty, state.iobData.isNotEmpty, let openAPSSettings = state.openAPSSettings { importResetSettingsView(token: token, settings: openAPSSettings) } else { + // New iAPS user onboardingView() } } else { From f446328dac1593f6114e192063208123a9b26fbe Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Sun, 11 Aug 2024 17:40:39 +0200 Subject: [PATCH 16/27] Bump version --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 5e898e3803..9f351c73ea 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -1,5 +1,5 @@ APP_DISPLAY_NAME = iAPS -APP_VERSION = 5.1.0 +APP_VERSION = 5.3.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = DEVELOPER_TEAM = ##TEAM_ID## From c17bc60cd60582e728e0e1b7d7e92d5151ef06bf Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Mon, 12 Aug 2024 13:45:08 +0200 Subject: [PATCH 17/27] change default max carbs --- FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json index 0b143452b6..5b5e65f22a 100644 --- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json +++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json @@ -7,7 +7,7 @@ "units": "mmol/L", "timeCap": 8, "useCalc": true, - "maxCarbs": 350, + "maxCarbs": 200, "birthDate": "0001-01-01T23:00:00.000Z", "displayHR": true, "closedLoop": false, From 162b29ea6919597bd182373d88201f2c35b8d48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Fri, 23 Aug 2024 20:35:14 +0200 Subject: [PATCH 18/27] Merge dev updates (manually due to merge conflicts). --- FreeAPS.xcodeproj/project.pbxproj | 4 +--- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 49abe64a4c..ebede2d475 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -1283,8 +1283,8 @@ children = ( 19AEF4312B1F5A98006FFE8B /* TIRView.swift */, 194D7E6D2B974F9F007A38C1 /* LoopsView.swift */, - 191A9D172BED24B000028D48 /* ActiveIOBView.swift */, 19DB70A62BF8F01E00C05381 /* ActiveCOBView.swift */, + 191A9D172BED24B000028D48 /* ActiveIOBView.swift */, ); path = Previews; sourceTree = ""; @@ -2992,7 +2992,6 @@ 19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */, 1967DFC229D053D300759F30 /* IconImage.swift in Sources */, 382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */, - 19493A3B2C5997AD00EC83A7 /* Database.swift in Sources */, 38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */, 38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */, 38AEE75225F022080013F05B /* SettingsManager.swift in Sources */, @@ -3248,7 +3247,6 @@ 19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */, 44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */, E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */, - 19493A3D2C59987700EC83A7 /* DatabaseModels.swift in Sources */, CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */, 38E8755427561E9800975559 /* DataFlow.swift in Sources */, 38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */, diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 6387d22b7c..1166ce5f4b 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -15,6 +15,8 @@ extension Home { @State var showCancelTTAlert = false @State var triggerUpdate = false @State var display = false + @State var displayGlucose = false + @State var displayGlucose = false let buttonFont = Font.custom("TimeButtonFont", size: 14) let viewPadding: CGFloat = 5 From b3c9df103ca946f0a25f31dd6b91e99dd7ada6e1 Mon Sep 17 00:00:00 2001 From: Merkaffe <72622342+Merkaffe@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:53:14 +0200 Subject: [PATCH 19/27] Update Config.xcconfig --- Config.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config.xcconfig b/Config.xcconfig index 9f351c73ea..a5e3c22298 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -2,8 +2,8 @@ APP_DISPLAY_NAME = iAPS APP_VERSION = 5.3.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = -DEVELOPER_TEAM = ##TEAM_ID## -BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS +DEVELOPER_TEAM = T7VZ6LU6H3 +BUNDLE_IDENTIFIER = com.jon.$(DEVELOPMENT_TEAM).aps APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroup APP_ICON = pod_colorful APP_URL_SCHEME = freeaps-x From b3f359f083220e5f12be7031a70e9455b2339b30 Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Mon, 26 Aug 2024 01:07:42 +0200 Subject: [PATCH 20/27] fix merge --- FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json index 5b5e65f22a..08b1222b17 100644 --- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json +++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json @@ -23,6 +23,7 @@ "uploadStats": true, "useAutotune": false, "useCalendar": false, + "displayDelta": false, "debugOptions": false, "displayDelta": false, "glucoseBadge": true, From b1a8e443ae9f2aa96b3b57db0abd7332bb62aada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Thu, 29 Aug 2024 10:50:13 +0200 Subject: [PATCH 21/27] merge dev branch updates --- .../Modules/Home/View/HomeRootView.swift | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 1166ce5f4b..9c9a9d8b4c 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -17,7 +17,6 @@ extension Home { @State var display = false @State var displayGlucose = false - @State var displayGlucose = false let buttonFont = Font.custom("TimeButtonFont", size: 14) let viewPadding: CGFloat = 5 @@ -725,7 +724,7 @@ extension Home { var body: some View { GeometryReader { geo in - if onboarded.first?.firstRun ?? true { + if onboarded.first?.firstRun ?? true { let token = state.token() // If old iAPS user pre v4.9.0 (not perfect yet) if state.glucose.isNotEmpty, state.iobData.isNotEmpty, let openAPSSettings = state.openAPSSettings { @@ -735,63 +734,63 @@ extension Home { onboardingView() } } else { - VStack(spacing: 0) { - // Header View - headerView(geo, extra: (displayGlucose && !state.skipGlucoseChart) ? 59 : 0) - ScrollView { - VStack { - // Main Chart - chart - // Adjust hours visible (X-Axis) - if state.timeSettings, !displayGlucose { timeSetting } - // TIR Chart - preview.padding(.top, (state.timeSettings && !displayGlucose) ? 5 : 15) - // Loops Chart - loopPreview.padding(.vertical, 15) - // COB Chart - if state.carbData > 0 { - activeCOBView - } - // IOB Chart - if state.iobs > 0 { - activeIOBView.padding(.top, state.carbData > 0 ? viewPadding : 0) - } - }.background { - // Track vertical scroll - GeometryReader { proxy in - let scrollPosition = proxy.frame(in: .named("HomeScrollView")).minY - let yThreshold: CGFloat = state.timeSettings ? -500 : -560 - Color.clear - .onChange(of: scrollPosition) { y in - if y < yThreshold, state.iobs > 0 || state.carbData > 0, !state.skipGlucoseChart { - withAnimation(.easeOut(duration: 0.3)) { displayGlucose = true } - } else { - withAnimation(.easeOut(duration: 0.4)) { displayGlucose = false } + VStack(spacing: 0) { + // Header View + headerView(geo, extra: (displayGlucose && !state.skipGlucoseChart) ? 59 : 0) + ScrollView { + VStack { + // Main Chart + chart + // Adjust hours visible (X-Axis) + if state.timeSettings, !displayGlucose { timeSetting } + // TIR Chart + preview.padding(.top, (state.timeSettings && !displayGlucose) ? 5 : 15) + // Loops Chart + loopPreview.padding(.vertical, 15) + // COB Chart + if state.carbData > 0 { + activeCOBView + } + // IOB Chart + if state.iobs > 0 { + activeIOBView.padding(.top, state.carbData > 0 ? viewPadding : 0) + } + }.background { + // Track vertical scroll + GeometryReader { proxy in + let scrollPosition = proxy.frame(in: .named("HomeScrollView")).minY + let yThreshold: CGFloat = state.timeSettings ? -500 : -560 + Color.clear + .onChange(of: scrollPosition) { y in + if y < yThreshold, state.iobs > 0 || state.carbData > 0, !state.skipGlucoseChart { + withAnimation(.easeOut(duration: 0.3)) { displayGlucose = true } + } else { + withAnimation(.easeOut(duration: 0.4)) { displayGlucose = false } + } } - } + } } - } - }.coordinateSpace(name: "HomeScrollView") - // Buttons - buttonPanel(geo) - } - .background( - colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white - .opacity(IAPSconfig.backgroundOpacity * 2) - ) - .ignoresSafeArea(edges: .vertical) - .overlay { - if let progress = state.bolusProgress, let amount = state.bolusAmount { - ZStack { - RoundedRectangle(cornerRadius: 15) - .fill(.gray.opacity(0.8)) - .frame(width: 320, height: 60) - bolusProgressView(progress: progress, amount: amount) + }.coordinateSpace(name: "HomeScrollView") + // Buttons + buttonPanel(geo) + } + .background( + colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white + .opacity(IAPSconfig.backgroundOpacity * 2) + ) + .ignoresSafeArea(edges: .vertical) + .overlay { + if let progress = state.bolusProgress, let amount = state.bolusAmount { + ZStack { + RoundedRectangle(cornerRadius: 15) + .fill(.gray.opacity(0.8)) + .frame(width: 320, height: 60) + bolusProgressView(progress: progress, amount: amount) + } } } } - } } .onAppear { configureView() From c2ebba934cadfaf236471026ecaa1449a33b6971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Thu, 29 Aug 2024 11:50:16 +0200 Subject: [PATCH 22/27] dev updates --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 9c9a9d8b4c..992ef1e229 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -788,6 +788,8 @@ extension Home { .frame(width: 320, height: 60) bolusProgressView(progress: progress, amount: amount) } + .frame(maxWidth: .infinity, alignment: .center) + .offset(x: 0, y: -100) } } } From 33ee5fa7c4561b188683781ee7970ca9f13aba8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:15:35 +0200 Subject: [PATCH 23/27] Remove my Bundle ID --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index a5e3c22298..7043f84959 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -3,7 +3,7 @@ APP_VERSION = 5.3.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = DEVELOPER_TEAM = T7VZ6LU6H3 -BUNDLE_IDENTIFIER = com.jon.$(DEVELOPMENT_TEAM).aps +BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroup APP_ICON = pod_colorful APP_URL_SCHEME = freeaps-x From 12c158195058662139d8b99f9f55ab69e17107f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:51:37 +0200 Subject: [PATCH 24/27] Update Config.xcconfig --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 7043f84959..9f351c73ea 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -2,7 +2,7 @@ APP_DISPLAY_NAME = iAPS APP_VERSION = 5.3.0 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = -DEVELOPER_TEAM = T7VZ6LU6H3 +DEVELOPER_TEAM = ##TEAM_ID## BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroup APP_ICON = pod_colorful From 02d8b2217d290349cfc10b693e8f4033b8d8c015 Mon Sep 17 00:00:00 2001 From: "Jon B.M" Date: Mon, 23 Sep 2024 14:58:24 +0200 Subject: [PATCH 25/27] merge fix and update defaults --- .../defaults/freeaps/freeaps_settings.json | 3 +- .../Modules/Home/View/HomeRootView.swift | 103 +++++++++--------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json index 9558a71044..fcefe58ef5 100644 --- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json +++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json @@ -7,6 +7,7 @@ "units": "mmol/L", "timeCap": 8, "useCalc": true, + "profileID": "Hypo Treatment", "maxCarbs": 200, "birthDate": "0001-01-01T23:00:00.000Z", "displayHR": true, @@ -26,10 +27,8 @@ "displayDelta": false, "debugOptions": false, "glucoseBadge": true, - "timeSettings": true, "smoothGlucose": false, "uploadGlucose": true, - "uploadVersion": true, "useAlarmSound": false, "displayOnWatch": "BGTarget", "minuteInterval": 30, diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index e063ca2b29..ea3fa55efd 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -744,64 +744,65 @@ extension Home { onboardingView() } } else { - VStack(spacing: 0) { - // Header View - headerView(geo) - - ScrollView { - VStack { - // Main Chart - chart - // Adjust hours visible (X-Axis) - timeSetting - // TIR Chart - if !state.glucose.isEmpty { - preview.padding(.top, 15) - } - // Loops Chart - loopPreview.padding(.vertical, 15) + VStack(spacing: 0) { + // Header View + headerView(geo) + + ScrollView { + VStack { + // Main Chart + chart + // Adjust hours visible (X-Axis) + timeSetting + // TIR Chart + if !state.glucose.isEmpty { + preview.padding(.top, 15) + } + // Loops Chart + loopPreview.padding(.vertical, 15) - if state.carbData > 0 { - activeCOBView - } + if state.carbData > 0 { + activeCOBView + } - // IOB Chart - if state.iobs > 0 { - activeIOBView - } + // IOB Chart + if state.iobs > 0 { + activeIOBView + } - }.background { - // Track vertical scroll - GeometryReader { proxy in - let scrollPosition = proxy.frame(in: .named("HomeScrollView")).minY - let yThreshold: CGFloat = -550 - Color.clear - .onChange(of: scrollPosition) { y in - if y < yThreshold, state.iobs > 0 || state.carbData > 0, !state.skipGlucoseChart { - withAnimation(.easeOut(duration: 0.3)) { displayGlucose = true } - } else { - withAnimation(.easeOut(duration: 0.4)) { displayGlucose = false } + }.background { + // Track vertical scroll + GeometryReader { proxy in + let scrollPosition = proxy.frame(in: .named("HomeScrollView")).minY + let yThreshold: CGFloat = -550 + Color.clear + .onChange(of: scrollPosition) { y in + if y < yThreshold, state.iobs > 0 || state.carbData > 0, !state.skipGlucoseChart { + withAnimation(.easeOut(duration: 0.3)) { displayGlucose = true } + } else { + withAnimation(.easeOut(duration: 0.4)) { displayGlucose = false } + } } } } - }.coordinateSpace(name: "HomeScrollView") - // Buttons - buttonPanel(geo) - } - - .background( - colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white - .opacity(IAPSconfig.backgroundOpacity * 2) - ) - .ignoresSafeArea(edges: .vertical) - .overlay { - if let progress = state.bolusProgress, let amount = state.bolusAmount { - ZStack { - RoundedRectangle(cornerRadius: 15) - .fill(.gray.opacity(0.8)) - .frame(width: 320, height: 60) - bolusProgressView(progress: progress, amount: amount) + }.coordinateSpace(name: "HomeScrollView") + // Buttons + buttonPanel(geo) + } + .background( + colorScheme == .light ? .gray.opacity(IAPSconfig.backgroundOpacity * 2) : .white + .opacity(IAPSconfig.backgroundOpacity * 2) + ) + .ignoresSafeArea(edges: .vertical) + .overlay { + if let progress = state.bolusProgress, let amount = state.bolusAmount { + ZStack { + RoundedRectangle(cornerRadius: 15) + .fill(.gray.opacity(0.8)) + .frame(width: 320, height: 60) + bolusProgressView(progress: progress, amount: amount) + } } } } From 1dd30447a0d5a6d47635d40dd72ee3a9857fa0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:25:12 +0200 Subject: [PATCH 26/27] Merge fix --- FreeAPS/Sources/Modules/Home/View/HomeRootView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index ea3fa55efd..376d27865c 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -803,6 +803,8 @@ extension Home { .frame(width: 320, height: 60) bolusProgressView(progress: progress, amount: amount) } + .frame(maxWidth: .infinity, alignment: .center) + .offset(x: 0, y: -100) } } } From c83f0cb3d4d16ba5ddb92b29aa9b50692995e564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Tue, 22 Oct 2024 22:25:20 +0200 Subject: [PATCH 27/27] merge fix --- .../Modules/Home/View/HomeRootView.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 1b57e12f0b..d4ee16540e 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -876,5 +876,29 @@ extension Home { } } } + + func onboardingView() -> some View { + Restore.RootView( + resolver: resolver, + int: 0, + profile: "default", + inSitu: false, + id_: "", + uniqueID: "", + openAPS: nil + ) + } + + func importResetSettingsView(token: String, settings: Preferences) -> some View { + Restore.RootView( + resolver: resolver, + int: -1, + profile: "default", + inSitu: false, + id_: token, + uniqueID: token, + openAPS: settings + ) + } } }