Procházet zdrojové kódy

Merge branch 'dev' into feat/dev-medtrum

Mike Plante před 2 měsíci
rodič
revize
a372f12375
31 změnil soubory, kde provedl 2366 přidání a 157 odebrání
  1. 6 3
      .github/workflows/unit_tests.yml
  2. 1 1
      Config.xcconfig
  3. 36 0
      Trio.xcodeproj/project.pbxproj
  4. 12 0
      Trio/Resources/InfoPlist.xcstrings
  5. 3 0
      Trio/Sources/Helpers/PropertyPersistentFlags.swift
  6. 2 1
      Trio/Sources/Helpers/PropertyWrappers/PersistedProperty.swift
  7. 15 0
      Trio/Sources/Helpers/String+Extensions.swift
  8. 499 1
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  9. 5 5
      Trio/Sources/Models/DecimalPickerSettings.swift
  10. 21 0
      Trio/Sources/Models/ExportSetting.swift
  11. 4 4
      Trio/Sources/Modules/History/View/CarbEntryEditorView.swift
  12. 7 4
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  13. 14 14
      Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift
  14. 4 0
      Trio/Sources/Modules/Settings/SettingsStateModel.swift
  15. 11 63
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  16. 5 0
      Trio/Sources/Modules/SettingsExport/SettingsExportDataFlow.swift
  17. 3 0
      Trio/Sources/Modules/SettingsExport/SettingsExportProvider.swift
  18. 1329 0
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  19. 192 0
      Trio/Sources/Modules/SettingsExport/View/SettingsExportRootView.swift
  20. 4 4
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  21. 4 4
      Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift
  22. 32 10
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  23. 3 0
      Trio/Sources/Router/Screen.swift
  24. 12 7
      Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  25. 1 0
      Trio/Sources/Services/OnboardingManager/OnboardingManager.swift
  26. 9 3
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  27. 1 1
      Trio/Sources/Shortcuts/Bolus/BolusIntent.swift
  28. 1 1
      Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift
  29. 59 17
      Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  30. 12 14
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  31. 59 0
      TrioTests/SettingsExportTests.swift

+ 6 - 3
.github/workflows/unit_tests.yml

@@ -28,7 +28,7 @@ jobs:
 
     steps:
       - name: Select Xcode version
-        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
+        run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
 
       - name: Checkout code
         uses: actions/checkout@v4
@@ -55,13 +55,16 @@ jobs:
           echo "📂 Contents of .build:"
           ls -lah .build || echo ".build directory not found"
 
+      - name: List available simulators
+        run: xcrun simctl list devices available
+
       - name: Build for testing
         run: |
           set -o pipefail && \
           time xcodebuild build-for-testing \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
 
       - name: Check for uncommitted changes
         run: |
@@ -104,7 +107,7 @@ jobs:
           time xcodebuild test-without-building \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
             $([ "$ENABLE_PARALLEL_TESTING" = "true" ] && echo "-parallel-testing-enabled YES") \
             2>&1 | tee xcodebuild.log
 

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.44
+APP_DEV_VERSION = 0.6.0.51
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 36 - 0
Trio.xcodeproj/project.pbxproj

@@ -458,6 +458,10 @@
 		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */; };
+		C2AA6CF62E1A734A00BF6C16 /* SettingsExportRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */; };
+		C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */; };
+		C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */; };
+		C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
@@ -573,6 +577,7 @@
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
 		DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DD3C47B22DC5608A003DD20D /* newerSuggested.json */; };
 		DD3C47B52DC57E06003DD20D /* MainMigrationErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */; };
+		DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3D60302F0377350021A33B /* ExportSetting.swift */; };
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
 		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
@@ -1287,6 +1292,10 @@
 		C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyPercentileChart.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatsSetup.swift; sourceTree = "<group>"; };
+		C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportRootView.swift; sourceTree = "<group>"; };
+		C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportDataFlow.swift; sourceTree = "<group>"; };
+		C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportProvider.swift; sourceTree = "<group>"; };
+		C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportStateModel.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -1407,6 +1416,7 @@
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD3C47B22DC5608A003DD20D /* newerSuggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = newerSuggested.json; sourceTree = "<group>"; };
 		DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMigrationErrorView.swift; sourceTree = "<group>"; };
+		DD3D60302F0377350021A33B /* ExportSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSetting.swift; sourceTree = "<group>"; };
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
 		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
@@ -1903,6 +1913,7 @@
 				E592A3762CEEC038009A472C /* ContactImage */,
 				9E56E3626FAD933385101B76 /* History */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
+				C2AA6CF52E1A734A00BF6C16 /* SettingsExport */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
 				F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */,
 				F90692CD274B99850037068D /* HealthKit */,
@@ -2364,6 +2375,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DD3D60302F0377350021A33B /* ExportSetting.swift */,
 				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
 				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
@@ -3062,6 +3074,25 @@
 			path = "Trio Watch App";
 			sourceTree = "<group>";
 		};
+		C2AA6CF12E1A734A00BF6C16 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		C2AA6CF52E1A734A00BF6C16 /* SettingsExport */ = {
+			isa = PBXGroup;
+			children = (
+				C2AA6CF12E1A734A00BF6C16 /* View */,
+				C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */,
+				C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */,
+				C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */,
+			);
+			path = SettingsExport;
+			sourceTree = "<group>";
+		};
 		C2C98283C436DB934D7E7994 /* Treatments */ = {
 			isa = PBXGroup;
 			children = (
@@ -4410,6 +4441,10 @@
 				DD6A4E842DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
+				C2AA6CF62E1A734A00BF6C16 /* SettingsExportRootView.swift in Sources */,
+				C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */,
+				C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */,
+				C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
 				CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */,
@@ -4486,6 +4521,7 @@
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
+				DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */,
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,

+ 12 - 0
Trio/Resources/InfoPlist.xcstrings

@@ -457,6 +457,18 @@
         }
       }
     },
+    "NSCalendarsFullAccessUsageDescription" : {
+      "comment" : "Privacy - Calendars Full Access Usage Description",
+      "extractionState" : "extracted_with_value",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
+          }
+        }
+      }
+    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

+ 3 - 0
Trio/Sources/Helpers/PropertyPersistentFlags.swift

@@ -23,4 +23,7 @@ final class PropertyPersistentFlags {
     @PersistedProperty(key: "diagnosticsSharing") var diagnosticsSharingEnabled: Bool?
 
     @PersistedProperty(key: "lastCleanupDate") var lastCleanupDate: Date?
+
+    // TODO: This flag can be deleted in March 2027. Check the commit for other places to cleanup.
+    @PersistedProperty(key: "hasSeenFatProteinOrderChange") var hasSeenFatProteinOrderChange: Bool?
 }

+ 2 - 1
Trio/Sources/Helpers/PropertyWrappers/PersistedProperty.swift

@@ -122,7 +122,8 @@ enum FileProtectionFixer {
         let flagFiles = [
             "onboardingCompleted.plist",
             "diagnosticsSharing.plist",
-            "lastCleanupDate.plist"
+            "lastCleanupDate.plist",
+            "hasSeenFatProteinOrderChange.plist"
         ]
 
         let fileManager = FileManager.default

+ 15 - 0
Trio/Sources/Helpers/String+Extensions.swift

@@ -8,6 +8,21 @@ extension String {
     mutating func capitalizeFirstLetter() {
         self = capitalizingFirstLetter()
     }
+
+    func formattedHourMinuteFromTimeString() -> String {
+        let input = DateFormatter()
+        input.dateFormat = "HH:mm:ss"
+
+        let output = DateFormatter()
+        output.timeStyle = .short
+        output.dateStyle = .none
+
+        guard let date = input.date(from: self) else {
+            return self
+        }
+
+        return output.string(from: date)
+    }
 }
 
 extension LosslessStringConvertible {

+ 499 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -8227,6 +8227,7 @@
       }
     },
     "%@ and %@ g fat" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -8351,6 +8352,7 @@
       }
     },
     "%@ and %@ g protein" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -8474,6 +8476,130 @@
         }
       }
     },
+    "%@ and %lld g fat" : {
+      "comment" : "The text to be displayed when fat is > 0 for the Add Carbs shortcut",
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ og %2$lld g fedt"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ und %2$lld g Fett"
+          }
+        },
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "%1$@ and %2$lld g fat"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ et %2$lld g de lipides"
+          }
+        },
+        "it" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ e %2$lld g di grassi"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ og %2$lld g fett"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ i %2$lld g tłuszczu"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ và %2$lld g chất béo"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ 和 %2$lld 克脂肪"
+          }
+        }
+      }
+    },
+    "%@ and %lld g protein" : {
+      "comment" : "The text to be displayed when protein is > 0 for the Add Carbs shortcut",
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ og %2$lld g protein"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ und %2$lld g Eiweiß"
+          }
+        },
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "%1$@ and %2$lld g protein"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ et %2$lld g de protéines"
+          }
+        },
+        "it" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ e %2$lld g di proteine"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ og %2$lld g protein"
+          }
+        },
+        "nl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ en %2$lld gram eiwit"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ i %2$lld g białka"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ và %2$lld g chất đạm"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ 和 %2$lld 克蛋白質"
+          }
+        }
+      }
+    },
     "%@ at %@" : {
       "localizations" : {
         "bg" : {
@@ -9320,6 +9446,22 @@
         }
       }
     },
+    "%@?" : {
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%@ ?"
+          }
+        },
+        "he" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "?%@"
+          }
+        }
+      }
+    },
     "%@/U" : {
       "localizations" : {
         "bg" : {
@@ -10037,7 +10179,6 @@
       }
     },
     "%lld h" : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -25810,6 +25951,7 @@
       }
     },
     "Add %@ grams of carbs?" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26053,6 +26195,72 @@
         }
       }
     },
+    "Add %lld g carbs" : {
+      "comment" : "A confirmation message that asks the user to confirm adding a certain amount of carbs, optionally including fat and protein.",
+      "isCommentAutoGenerated" : true,
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tilføj %lld gram kulhydrat"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%lld g Kohlenhydrate hinzufügen"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Confirmez-vous l'ajout de %lld grammes of glucides"
+          }
+        },
+        "he" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "הוספת %lld גרם פחמימות"
+          }
+        },
+        "it" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Aggiungere %lld grammi di carboidrati"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Legge til %lld gram karbohydrater"
+          }
+        },
+        "nl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%lld gram koolhydraten toevoegen"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Dodać %lld gramów węglowodanów"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Thêm %lld grams carbs"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "要加入 %lld 克碳水化合物嗎"
+          }
+        }
+      }
+    },
     "Add a CGM and pump to enable automated insulin delivery" : {
       "localizations" : {
         "bg" : {
@@ -31207,6 +31415,7 @@
       }
     },
     "Added %@ g carbs" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -31324,6 +31533,65 @@
         }
       }
     },
+    "Added %lld g carbs" : {
+      "comment" : "A message that confirms the adding of carbs via the Add Carbs shortcut",
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tilføjet %lld g kulhydrater"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%lld g Kohlenhydrate hinzugefügt"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%lld g de glucides ont été ajoutés"
+          }
+        },
+        "it" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Aggiunti %lld g carboidrati"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "La til %lld g karbohydrater"
+          }
+        },
+        "nl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Toegevoegd %lld gr koolhydraten"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Dodano %lld g węglowodanów"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Đã thêm %lld g carbs"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "已加入 %lld 克碳水化合物"
+          }
+        }
+      }
+    },
     "Additional notification types" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -36161,6 +36429,9 @@
         }
       }
     },
+    "Allow Fetching From Nightscout" : {
+
+    },
     "Allow Notifications" : {
       "localizations" : {
         "bg" : {
@@ -43699,6 +43970,9 @@
         }
       }
     },
+    "App Version" : {
+
+    },
     "Appearance" : {
       "localizations" : {
         "bg" : {
@@ -51838,6 +52112,9 @@
         }
       }
     },
+    "Basal Rate (%@)" : {
+
+    },
     "Basal Rate Adjustment" : {
       "localizations" : {
         "bg" : {
@@ -57476,6 +57753,9 @@
         }
       }
     },
+    "Branch" : {
+
+    },
     "BRANCH: %@" : {
       "localizations" : {
         "bg" : {
@@ -57594,6 +57874,9 @@
         }
       }
     },
+    "Build Number" : {
+
+    },
     "By default, Trio collects crash reports and other anonymized data related to errors, exceptions, and overall app performance." : {
       "localizations" : {
         "bg" : {
@@ -60734,6 +61017,9 @@
         }
       }
     },
+    "Carb Ratio (%@)" : {
+
+    },
     "Carb Ratios" : {
       "comment" : "Carb Ratios header",
       "localizations" : {
@@ -69969,6 +70255,7 @@
       }
     },
     "Confirm Before logging" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -70086,6 +70373,36 @@
         }
       }
     },
+    "Confirm Before Logging" : {
+      "comment" : "Label for a toggle switch that allows the user to confirm before logging a meal.",
+      "isCommentAutoGenerated" : true,
+      "localizations" : {
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Bekreft før lagring"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Potwierdź przed zalogowaniem"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Xác nhận trước khi ghi lại"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "登錄前確認"
+          }
+        }
+      }
+    },
     "Confirm Bolus Faster" : {
       "localizations" : {
         "bg" : {
@@ -72840,6 +73157,9 @@
         }
       }
     },
+    "Could not access documents directory" : {
+
+    },
     "Count" : {
       "localizations" : {
         "bg" : {
@@ -107449,6 +107769,18 @@
         }
       }
     },
+    "Export Categories" : {
+
+    },
+    "Export Date" : {
+
+    },
+    "Export Error" : {
+
+    },
+    "Export failed: %@" : {
+
+    },
     "Export logs" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -107699,6 +108031,12 @@
         }
       }
     },
+    "Export Settings" : {
+      "comment" : "Export Settings menu item in Trio Settings Root View"
+    },
+    "Exporting..." : {
+
+    },
     "Extended" : {
       "comment" : "Title string for BeepPreference.extended",
       "extractionState" : "manual",
@@ -110139,6 +110477,9 @@
         }
       }
     },
+    "Failed to write export file: %@" : {
+
+    },
     "Fat" : {
       "comment" : "Add Fat",
       "localizations" : {
@@ -125135,6 +125476,9 @@
         }
       }
     },
+    "High Temptarget Raises Sensitivity" : {
+
+    },
     "High Threshold" : {
       "localizations" : {
         "bg" : {
@@ -126711,6 +127055,7 @@
       }
     },
     "How many grams of carbs did you eat?" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -126947,6 +127292,7 @@
       }
     },
     "How many grams of fat did you eat?" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -127064,7 +127410,60 @@
         }
       }
     },
+    "How many grams of fat?" : {
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hvor mange gram fedt?"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Wie viele Gramm Fett?"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Combien de grammes de lipides (graisses) ?"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hvor mange gram fett?"
+          }
+        },
+        "nl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hoeveel gram vet?"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ile gramów tłuszczu?"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Bao nhiêu gam chất béo?"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "您吃了多少克脂肪?"
+          }
+        }
+      }
+    },
     "How many grams of protein did you eat?" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -127182,6 +127581,60 @@
         }
       }
     },
+    "How many grams of protein?" : {
+      "comment" : "Request dialog text for the \"Quantity Protein\" parameter.",
+      "isCommentAutoGenerated" : true,
+      "localizations" : {
+        "da" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hvor mange gram protein?"
+          }
+        },
+        "de" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Wie viele Gramm Eiweiß?"
+          }
+        },
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Combien de grammes de protéines ?"
+          }
+        },
+        "nb-NO" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hvor mange gram protein?"
+          }
+        },
+        "nl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hoeveel gram eiwit?"
+          }
+        },
+        "pl" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ile gramów białka?"
+          }
+        },
+        "vi" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Bao nhiêu gam carbs?"
+          }
+        },
+        "zh-Hant" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "您吃了多少克蛋白質?"
+          }
+        }
+      }
+    },
     "How Trio Manages Contact Images" : {
       "localizations" : {
         "bg" : {
@@ -133280,6 +133733,9 @@
         }
       }
     },
+    "Indefinite" : {
+
+    },
     "Indicates glucose smoothing is enabled." : {
       "localizations" : {
         "bg" : {
@@ -135695,6 +136151,9 @@
         }
       }
     },
+    "Insulin Type" : {
+
+    },
     "Intercept" : {
       "localizations" : {
         "bg" : {
@@ -137868,6 +138327,12 @@
         }
       }
     },
+    "ISF (%@)" : {
+
+    },
+    "ISF and CR" : {
+
+    },
     "ISF/CR" : {
       "comment" : "Option for both ISF and CR",
       "localizations" : {
@@ -145868,6 +146333,15 @@
         }
       }
     },
+    "Logging Failed: Max Carbs = %lld g" : {
+      "comment" : "An error message that the Add Carbs shortcut failed because the entered carbs exceeded the Max Carbs setting"
+    },
+    "Logging Failed: Max Fat = %lld g" : {
+      "comment" : "An error message that the Add Carbs shortcut failed because the entered fat exceeded the Max Fat setting"
+    },
+    "Logging Failed: Max Protein = %lld g" : {
+      "comment" : "An error message that the Add Carbs shortcut failed because the entered protein exceeded the Max Protein setting"
+    },
     "Loop at %@ failed." : {
       "localizations" : {
         "bg" : {
@@ -148884,6 +149358,9 @@
         }
       }
     },
+    "Low Temptarget Lowers Sensitivity" : {
+
+    },
     "Low Threshold" : {
       "localizations" : {
         "bg" : {
@@ -158215,6 +158692,9 @@
         }
       }
     },
+    "Metadata" : {
+
+    },
     "Meter glucose" : {
       "comment" : "When adding capillary glucose meater reading",
       "localizations" : {
@@ -168702,6 +169182,9 @@
         }
       }
     },
+    "Not Connected" : {
+
+    },
     "Not enough glucose data. You need at least three glucose readings in the last six hours to run the algorithm." : {
       "localizations" : {
         "bg" : {
@@ -188994,6 +189477,9 @@
         }
       }
     },
+    "Pump Type" : {
+
+    },
     "Quantity Carbs" : {
       "localizations" : {
         "bg" : {
@@ -220636,6 +221122,9 @@
         }
       }
     },
+    "Suspend Zeros IOB" : {
+
+    },
     "suspend-end" : {
       "localizations" : {
         "bg" : {
@@ -223253,6 +223742,9 @@
         }
       }
     },
+    "Target (%@)" : {
+
+    },
     "Target Behavior" : {
       "localizations" : {
         "bg" : {
@@ -229722,6 +230214,9 @@
         }
       }
     },
+    "The order of Fat and Protein inputs has changed." : {
+
+    },
     "The oref algorithm determines insulin dosing based on a number of scenarios that it estimates with different types of forecasts." : {
       "localizations" : {
         "bg" : {
@@ -246231,6 +246726,9 @@
         }
       }
     },
+    "Trio Backup" : {
+
+    },
     "Trio calculates your current Insulin On Board (IOB) from:" : {
       "localizations" : {
         "bg" : {

+ 5 - 5
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -43,7 +43,7 @@ struct DecimalPickerSettings {
         max: 1.2,
         type: PickerSetting.PickerSettingType.factor
     )
-    var high = PickerSetting(value: 180, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
+    var high = PickerSetting(value: 180, step: 1, min: 100, max: 400, type: PickerSetting.PickerSettingType.glucose)
     var low = PickerSetting(value: 70, step: 1, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
     var maxCarbs = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxFat = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
@@ -51,7 +51,7 @@ struct DecimalPickerSettings {
     var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.05, max: 1.5, type: PickerSetting.PickerSettingType.factor)
     var fattyMealFactor = PickerSetting(value: 0.7, step: 0.05, min: 0.05, max: 1, type: PickerSetting.PickerSettingType.factor)
     var sweetMealFactor = PickerSetting(value: 1, step: 0.05, min: 0.05, max: 2, type: PickerSetting.PickerSettingType.factor)
-    var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxDailySafetyMultiplier = PickerSetting(
         value: 3,
         step: 0.1,
@@ -87,8 +87,8 @@ struct DecimalPickerSettings {
         type: PickerSetting.PickerSettingType.factor
     )
     var remainingCarbsCap = PickerSetting(value: 90, step: 5, min: 0, max: 200, type: PickerSetting.PickerSettingType.gram)
-    var maxSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 30, max: 180, type: PickerSetting.PickerSettingType.minute)
-    var maxUAMSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 30, max: 180, type: PickerSetting.PickerSettingType.minute)
+    var maxSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 15, max: 180, type: PickerSetting.PickerSettingType.minute)
+    var maxUAMSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 15, max: 180, type: PickerSetting.PickerSettingType.minute)
     var smbInterval = PickerSetting(value: 3, step: 1, min: 1, max: 10, type: PickerSetting.PickerSettingType.minute)
     var bolusIncrement = PickerSetting(
         value: 0.1,
@@ -113,7 +113,7 @@ struct DecimalPickerSettings {
         max: 0.4,
         type: PickerSetting.PickerSettingType.factor
     )
-    var adjustmentFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.3, max: 1.5, type: PickerSetting.PickerSettingType.factor)
+    var adjustmentFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.3, max: 3.0, type: PickerSetting.PickerSettingType.factor)
     var adjustmentFactorSigmoid = PickerSetting(
         value: 0.5,
         step: 0.05,

+ 21 - 0
Trio/Sources/Models/ExportSetting.swift

@@ -0,0 +1,21 @@
+struct ExportSetting: Codable {
+    let category: String
+    let subcategory: String
+    let name: String
+    let value: String
+    let unit: String
+
+    init(category: String, subcategory: String = "", name: String, value: String, unit: String = "") {
+        self.category = category
+        self.subcategory = subcategory
+        self.name = name
+        self.value = value
+        self.unit = unit
+    }
+}
+
+struct ExportSettingPayload: Codable {
+    let exportFormat: String
+    let exportDate: String
+    let settings: [ExportSetting]
+}

+ 4 - 4
Trio/Sources/Modules/History/View/CarbEntryEditorView.swift

@@ -145,9 +145,9 @@ struct CarbEntryEditorView: View {
 
                     if state.settingsManager.settings.useFPUconversion {
                         HStack {
-                            Text("Protein")
+                            Text("Fat")
                             TextFieldWithToolBar(
-                                text: $editedProtein,
+                                text: $editedFat,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
                                 numberFormatter: mealFormatter,
@@ -156,9 +156,9 @@ struct CarbEntryEditorView: View {
                         }
 
                         HStack {
-                            Text("Fat")
+                            Text("Protein")
                             TextFieldWithToolBar(
-                                text: $editedFat,
+                                text: $editedProtein,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
                                 numberFormatter: mealFormatter,

+ 7 - 4
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -137,11 +137,14 @@ struct CurrentGlucoseView: View {
             return "--"
         }
 
-        let lastGlucose = glucose.last?.glucose ?? 0
-        let secondLastGlucose = glucose.first?.glucose ?? 0
+        var lastGlucose = Decimal(glucose.last?.glucose ?? 0)
+        var secondLastGlucose = Decimal(glucose.first?.glucose ?? 0)
+        if units == .mmolL {
+            lastGlucose = lastGlucose.asMmolL
+            secondLastGlucose = secondLastGlucose.asMmolL
+        }
         let delta = lastGlucose - secondLastGlucose
-        let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
-        return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
+        return deltaFormatter.string(from: delta as NSNumber) ?? "--"
     }
 }
 

+ 14 - 14
Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift

@@ -81,26 +81,26 @@ extension MealSettings {
                             if state.useFPUconversion {
                                 VStack {
                                     HStack {
-                                        Text("Max Protein")
+                                        Text("Max Fat")
 
                                         Spacer()
 
                                         Group {
-                                            Text(state.maxProtein.description)
-                                                .foregroundColor(!displayPickerMaxProtein ? .primary : .accentColor)
+                                            Text(state.maxFat.description)
+                                                .foregroundColor(!displayPickerMaxFat ? .primary : .accentColor)
 
                                             Text(" g").foregroundColor(.secondary)
                                         }
                                     }
                                     .onTapGesture {
-                                        displayPickerMaxProtein.toggle()
+                                        displayPickerMaxFat.toggle()
                                     }
                                 }
                                 .padding(.top)
 
-                                if displayPickerMaxProtein {
-                                    let setting = PickerSettingsProvider.shared.settings.maxProtein
-                                    Picker(selection: $state.maxProtein, label: Text("")) {
+                                if displayPickerMaxFat {
+                                    let setting = PickerSettingsProvider.shared.settings.maxFat
+                                    Picker(selection: $state.maxFat, label: Text("")) {
                                         ForEach(
                                             PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
                                             id: \.self
@@ -114,26 +114,26 @@ extension MealSettings {
 
                                 VStack {
                                     HStack {
-                                        Text("Max Fat")
+                                        Text("Max Protein")
 
                                         Spacer()
 
                                         Group {
-                                            Text(state.maxFat.description)
-                                                .foregroundColor(!displayPickerMaxFat ? .primary : .accentColor)
+                                            Text(state.maxProtein.description)
+                                                .foregroundColor(!displayPickerMaxProtein ? .primary : .accentColor)
 
                                             Text(" g").foregroundColor(.secondary)
                                         }
                                     }
                                     .onTapGesture {
-                                        displayPickerMaxFat.toggle()
+                                        displayPickerMaxProtein.toggle()
                                     }
                                 }
                                 .padding(.top)
 
-                                if displayPickerMaxFat {
-                                    let setting = PickerSettingsProvider.shared.settings.maxFat
-                                    Picker(selection: $state.maxFat, label: Text("")) {
+                                if displayPickerMaxProtein {
+                                    let setting = PickerSettingsProvider.shared.settings.maxProtein
+                                    Picker(selection: $state.maxProtein, label: Text("")) {
                                         ForEach(
                                             PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
                                             id: \.self

+ 4 - 0
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,3 +1,5 @@
+import CoreData
+import Foundation
 import LoopKit
 import LoopKitUI
 import SwiftUI
@@ -10,6 +12,8 @@ extension Settings {
         @Injected() private var nightscoutManager: NightscoutManager!
         @Injected() var pluginManager: PluginManager!
         @Injected() var fetchCgmManager: FetchGlucoseManager!
+        @Injected() private var storage: FileStorage!
+        @Injected() var overrideStorage: OverrideStorage!
 
         @Published var units: GlucoseUnits = .mgdL
         @Published var closedLoop = false

+ 11 - 63
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -267,6 +267,17 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
 
+                    Section(
+                        header: Text("Trio Backup"),
+                        content: {
+                            Text(String(
+                                localized: "Export Settings",
+                                comment: "Export Settings menu item in Trio Settings Root View"
+                            ))
+                                .navigationLink(to: .settingsExport, from: self)
+                        }
+                    ).listRowBackground(Color.chart)
+
                 } else {
                     Section(
                         header: Text("Search Results"),
@@ -293,69 +304,6 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
                 }
-
-                // TODO: remove this more or less entirely; add build-time flag to enable Middleware; add settings export feature
-//                Section {
-//                    Toggle("Developer Options", isOn: $state.debugOptions)
-//                    if state.debugOptions {
-//                        Group {
-//                            HStack {
-//                                Text("NS Upload Profile and Settings")
-//                                Button("Upload") { state.uploadProfileAndSettings(true) }
-//                                    .frame(maxWidth: .infinity, alignment: .trailing)
-//                                    .buttonStyle(.borderedProminent)
-//                            }
-//                            // Commenting this out for now, as not needed and possibly dangerous for users to be able to nuke their pump pairing informations via the debug menu
-//                            // Leaving it in here, as it may be a handy functionality for further testing or developers.
-//                            // See https://github.com/nightscout/Trio/pull/277 for more information
-//                            //
-//                            //                            HStack {
-//                            //                                Text("Delete Stored Pump State Binary Files")
-//                            //                                Button("Delete") { state.resetLoopDocuments() }
-//                            //                                    .frame(maxWidth: .infinity, alignment: .trailing)
-//                            //                                    .buttonStyle(.borderedProminent)
-//                            //                            }
-//                        }
-//                        Group {
-//                            Text("Preferences")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.preferences), from: self)
-//                            Text("Pump Settings")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.settings), from: self)
-//                            Text("Autosense")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.autosense), from: self)
-//                            //                            Text("Pump History")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.pumpHistory), from: self)
-//                            Text("Basal profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.basalProfile), from: self)
-//                    Text("Targets ranges")
-//                        .navigationLink(to: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
-//                            Text("Temp targets")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
-//                        }
-//
-//                        Group {
-//                            Text("Pump profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
-//                            Text("Profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
-//                            //                            Text("Carbs")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.carbHistory), from: self)
-//                        }
-//
-//                        Group {
-//                            Text("Target presets")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.tempTargetsPresets), from: self)
-//                            Text("Calibrations")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.calibrations), from: self)
-//                            Text("Middleware")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self)
-//                            //                            Text("Statistics")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.statistics), from: self)
-//                            Text("Edit settings json")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.settings), from: self)
-//                        }
-//                    }
-//                }.listRowBackground(Color.chart)
             }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .sheet(isPresented: $shouldDisplayHint) {

+ 5 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportDataFlow.swift

@@ -0,0 +1,5 @@
+enum SettingsExport {
+    enum Config {}
+}
+
+protocol SettingsExportProvider: Provider {}

+ 3 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportProvider.swift

@@ -0,0 +1,3 @@
+extension SettingsExport {
+    final class Provider: BaseProvider, SettingsExportProvider {}
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1329 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift


+ 192 - 0
Trio/Sources/Modules/SettingsExport/View/SettingsExportRootView.swift

@@ -0,0 +1,192 @@
+import SwiftUI
+import Swinject
+
+extension SettingsExport {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        @State private var showSettingsExport = false
+        @State private var showExportError = false
+        @State private var exportErrorMessage = ""
+        @State private var exportedFileURL: URL?
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                Section(
+                    header: Text("Export Categories"),
+                    content: {
+                        // Select All toggle
+                        HStack {
+                            Button(action: {
+                                state.toggleAllCategories(!state.allCategoriesSelected)
+                            }) {
+                                HStack {
+                                    Image(systemName: state.allCategoriesSelected ? "checkmark.square.fill" : "square")
+                                        .foregroundColor(state.allCategoriesSelected ? .blue : .secondary)
+                                    Text(
+                                        state
+                                            .allCategoriesSelected ? String(localized: "Deselect All") :
+                                            String(localized: "Select All")
+                                    )
+                                    .fontWeight(.bold)
+                                    .foregroundColor(.primary)
+                                    Spacer()
+                                }
+                            }
+                            .buttonStyle(PlainButtonStyle())
+                        }
+
+                        // Individual category toggles
+                        ForEach(SettingsExport.StateModel.ExportCategory.allCases) { category in
+                            HStack {
+                                Button(action: {
+                                    if state.selectedCategories.contains(category) {
+                                        state.selectedCategories.remove(category)
+                                    } else {
+                                        state.selectedCategories.insert(category)
+                                    }
+                                }) {
+                                    HStack {
+                                        Image(
+                                            systemName: state.selectedCategories
+                                                .contains(category) ? "checkmark.square.fill" : "square"
+                                        )
+                                        .foregroundColor(state.selectedCategories.contains(category) ? .blue : .secondary)
+
+                                        Text(category.rawValue)
+
+                                        Spacer()
+                                    }
+                                }
+                                .buttonStyle(PlainButtonStyle())
+                            }
+                            .padding(.vertical, 2)
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                Section {
+                    Button(action: {
+                        Task {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.isExporting = true
+
+                            switch await state.exportSelectedSettings() {
+                            case let .success(fileURL):
+                                if FileManager.default.fileExists(atPath: fileURL.path) {
+                                    do {
+                                        let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
+                                        let fileSize = attributes[.size] as? Int ?? 0
+
+                                        if fileSize > 0 {
+                                            exportedFileURL = fileURL
+                                            // Stop spinner on successful export
+                                            state.isExporting = false
+                                            showSettingsExport = true
+                                        } else {
+                                            exportErrorMessage = "Export file is empty (0 bytes)"
+                                            showExportError = true
+                                            state.isExporting = false
+                                        }
+                                    } catch {
+                                        exportErrorMessage = "Could not verify file attributes: \(error.localizedDescription)"
+                                        showExportError = true
+                                        // Stop spinner on error
+                                        state.isExporting = false
+                                    }
+                                } else {
+                                    exportErrorMessage = "Export file was created but could not be found at: \(fileURL.path)"
+                                    showExportError = true
+                                    // Stop spinner on error
+                                    state.isExporting = false
+                                }
+                            case let .failure(error):
+                                exportErrorMessage = error.localizedDescription
+                                showExportError = true
+                                // Stop spinner on error
+                                state.isExporting = false
+                            }
+                        }
+                    }, label: {
+                        if state.isExporting {
+                            HStack {
+                                ProgressView().padding(.trailing, 10)
+                                Text("Exporting...")
+                            }
+                        } else {
+                            Text("Export Settings")
+                        }
+
+                    })
+                        .disabled(state.selectedCategories.isEmpty)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                }.listRowBackground(
+                    state.selectedCategories.isEmpty ? Color(.systemGray4) : Color(.systemBlue)
+                )
+            }
+            .listSectionSpacing(sectionSpacing)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationTitle("Export Settings")
+            .navigationBarTitleDisplayMode(.automatic)
+//            // TODO: implement help sheet
+//            .toolbar {
+//                ToolbarItem(placement: .topBarTrailing) {
+//                    Button(
+//                        action: {
+//                            state.isHelpSheetPresented.toggle()
+//                        },
+//                        label: {
+//                            Image(systemName: "questionmark.circle")
+//                        }
+//                    )
+//                }
+//            }
+//            .sheet(isPresented: $state.isHelpSheetPresented) {
+//                NavigationStack {
+//                    List {
+//                        Text("Hello World!")
+//                    }
+//                }
+//                .padding()
+//                .presentationDetents(
+//                    [.fraction(0.9), .large],
+//                    selection: $state.helpSheetDetent
+//                )
+//            }
+            .sheet(isPresented: $showSettingsExport) {
+                if let fileURL = exportedFileURL {
+                    ShareSheet(activityItems: [fileURL])
+                }
+            }
+            .alert("Export Error", isPresented: $showExportError) {
+                Button("OK", role: .cancel) {}
+            } message: {
+                Text(exportErrorMessage)
+            }
+        }
+    }
+}
+
+private struct ExportCategoryRow: View {
+    let title: String
+    let description: String
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(title)
+                .font(.subheadline)
+                .fontWeight(.medium)
+            Text(description)
+                .font(.caption)
+                .foregroundColor(.secondary)
+        }
+        .padding(.vertical, 2)
+    }
+}

+ 4 - 4
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -205,14 +205,14 @@ struct MealStatsView: View {
         }
         .chartForegroundStyleScale([
             "Carbs": Color.orange,
-            "Protein": Color.blue,
-            "Fat": Color.purple
+            "Fat": Color.purple,
+            "Protein": Color.blue
         ])
         .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
             let legendItems: [(String, Color)] = state.useFPUconversion ? [
                 (String(localized: "Carbs"), Color.orange),
-                (String(localized: "Protein"), Color.blue),
-                (String(localized: "Fat"), Color.purple)
+                (String(localized: "Fat"), Color.purple),
+                (String(localized: "Protein"), Color.blue)
             ] : [(String(localized: "Carbs"), Color.orange)]
 
             let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]

+ 4 - 4
Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift

@@ -83,10 +83,10 @@ struct AddMealPresetView: View {
 
     @ViewBuilder private func proteinAndFat() -> some View {
         HStack {
-            Text("Protein").foregroundColor(.red)
+            Text("Fat").foregroundColor(.orange)
             Spacer()
             TextFieldWithToolBar(
-                text: $presetProtein,
+                text: $presetFat,
                 placeholder: "0",
                 keyboardType: .numberPad,
                 numberFormatter: mealFormatter,
@@ -94,10 +94,10 @@ struct AddMealPresetView: View {
             )
         }
         HStack {
-            Text("Fat").foregroundColor(.orange)
+            Text("Protein").foregroundColor(.red)
             Spacer()
             TextFieldWithToolBar(
-                text: $presetFat,
+                text: $presetProtein,
                 placeholder: "0",
                 keyboardType: .numberPad,
                 numberFormatter: mealFormatter,

+ 32 - 10
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -24,6 +24,7 @@ extension Treatments {
         @State private var calculatorDetent = PresentationDetent.large
         @State private var pushed: Bool = false
         @State private var debounce: DispatchWorkItem?
+        @State private var showFatProteinOrderBanner = false
 
         private enum Config {
             static let dividerHeight: CGFloat = 2
@@ -85,35 +86,35 @@ extension Treatments {
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
                 HStack {
-                    Text("Protein")
+                    Text("Fat")
                     TextFieldWithToolBar(
-                        text: $state.protein,
+                        text: $state.fat,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         showArrows: true,
-                        previousTextField: { focusedField = previousField(from: .protein) },
-                        nextTextField: { focusedField = nextField(from: .protein) },
+                        previousTextField: { focusedField = previousField(from: .fat) },
+                        nextTextField: { focusedField = nextField(from: .fat) },
                         unitsText: String(localized: "g", comment: "Units for carbs")
                     )
-                    .focused($focusedField, equals: .protein)
+                    .focused($focusedField, equals: .fat)
                 }
 
                 Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
 
                 HStack {
-                    Text("Fat")
+                    Text("Protein")
                     TextFieldWithToolBar(
-                        text: $state.fat,
+                        text: $state.protein,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         showArrows: true,
-                        previousTextField: { focusedField = previousField(from: .fat) },
-                        nextTextField: { focusedField = nextField(from: .fat) },
+                        previousTextField: { focusedField = previousField(from: .protein) },
+                        nextTextField: { focusedField = nextField(from: .protein) },
                         unitsText: String(localized: "g", comment: "Units for carbs")
                     )
-                    .focused($focusedField, equals: .fat)
+                    .focused($focusedField, equals: .protein)
                 }
             }
         }
@@ -198,6 +199,23 @@ extension Treatments {
 
                             if state.useFPUconversion {
                                 proteinAndFat()
+
+                                if showFatProteinOrderBanner {
+                                    HStack {
+                                        Image(systemName: "arrow.left.arrow.right")
+                                        Text("The order of Fat and Protein inputs has changed.").font(.callout)
+                                        Spacer()
+                                        Button {
+                                            PropertyPersistentFlags.shared.hasSeenFatProteinOrderChange = true
+                                            withAnimation { showFatProteinOrderBanner = false }
+                                        } label: {
+                                            Image(systemName: "xmark.circle.fill")
+                                        }
+                                        .buttonStyle(.plain)
+                                    }
+                                    .listRowBackground(Color.orange.opacity(0.75))
+                                    .transition(.opacity)
+                                }
                             }
 
                             // Time
@@ -391,6 +409,10 @@ extension Treatments {
                     Task { @MainActor in
                         state.insulinCalculated = await state.calculateInsulin()
                     }
+
+                    if PropertyPersistentFlags.shared.hasSeenFatProteinOrderChange != true {
+                        showFatProteinOrderBanner = true
+                    }
                 }
             }
             .onDisappear {

+ 3 - 0
Trio/Sources/Router/Screen.swift

@@ -49,6 +49,7 @@ enum Screen: Identifiable, Hashable {
     case algorithmAdvancedSettings
     case unitsAndLimits
     case appDiagnostics
+    case settingsExport
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -162,6 +163,8 @@ extension Screen {
             UnitsLimitsSettings.RootView(resolver: resolver)
         case .appDiagnostics:
             AppDiagnostics.RootView(resolver: resolver)
+        case .settingsExport:
+            SettingsExport.RootView(resolver: resolver)
         }
     }
 

+ 12 - 7
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -39,20 +39,25 @@ extension LiveActivityAttributes.ContentState {
 
     static func calculateChange(chart: [GlucoseData], units: GlucoseUnits) -> String {
         guard chart.count > 2 else { return "" }
-        let lastGlucose = chart.first?.glucose ?? 0
-        let secondLastGlucose = chart.dropFirst().first?.glucose ?? 0
-        let delta = lastGlucose - secondLastGlucose
-        let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
+
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.maximumFractionDigits = 1
+        formatter.positivePrefix = "  +"
+        formatter.negativePrefix = "  -"
+
+        var lastGlucose = Decimal(chart.first?.glucose ?? 0)
+        var secondLastGlucose = Decimal(chart.dropFirst().first?.glucose ?? 0)
         if units == .mmolL {
+            lastGlucose = lastGlucose.asMmolL
+            secondLastGlucose = secondLastGlucose.asMmolL
+
             formatter.minimumFractionDigits = 1
             formatter.maximumFractionDigits = 1
         }
-        formatter.positivePrefix = "  +"
-        formatter.negativePrefix = "  -"
-        return formatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
+
+        let delta = lastGlucose - secondLastGlucose
+        return formatter.string(from: delta as NSNumber) ?? "--"
     }
 
     init(

+ 1 - 0
Trio/Sources/Services/OnboardingManager/OnboardingManager.swift

@@ -24,6 +24,7 @@ import Swinject
     /// Marks onboarding as completed and updates the shouldShowOnboarding flag.
     func completeOnboarding() {
         PropertyPersistentFlags.shared.onboardingCompleted = true
+        PropertyPersistentFlags.shared.hasSeenFatProteinOrderChange = true
         shouldShowOnboarding = false
     }
 

+ 9 - 3
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -320,12 +320,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
                 // Calculate delta if we have at least 2 readings
                 if glucoseObjects.count >= 2 {
-                    var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
-
+                    var glucoseLast = Decimal(glucoseObjects[0].glucose)
+                    var glucoseSecondLast = Decimal(glucoseObjects[1].glucose)
                     if self.units == .mmolL {
-                        deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                        glucoseLast = glucoseLast.asMmolL
+                        glucoseSecondLast = glucoseSecondLast.asMmolL
                     }
 
+                    let deltaValue = glucoseLast - glucoseSecondLast
                     let formattedDelta = Formatter.glucoseFormatter(for: self.units)
                         .string(from: deltaValue as NSNumber) ?? "0"
                     watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
@@ -740,6 +742,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
                 carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                 carbEntry.isUploadedToNS = false
+                carbEntry.isUploadedToHealth = false
+                carbEntry.isUploadedToTidepool = false
 
                 do {
                     guard context.hasChanges else {
@@ -802,6 +806,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
                     carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                     carbEntry.isUploadedToNS = false
+                    carbEntry.isUploadedToHealth = false
+                    carbEntry.isUploadedToTidepool = false
 
                     guard context.hasChanges else {
                         // Acknowledge failure

+ 1 - 1
Trio/Sources/Shortcuts/Bolus/BolusIntent.swift

@@ -3,7 +3,7 @@ import Foundation
 import Intents
 import Swinject
 
-@available(iOS 16.0,*) struct BolusIntent: AppIntent {
+struct BolusIntent: AppIntent {
     // Title of the action in the Shortcuts app
     static var title = LocalizedStringResource("Enact Bolus")
 

+ 1 - 1
Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift

@@ -2,7 +2,7 @@ import Combine
 import CoreData
 import Foundation
 
-@available(iOS 16.0,*) final class BolusIntentRequest: BaseIntentsRequest {
+final class BolusIntentRequest: BaseIntentsRequest {
     func bolus(_ bolusAmount: Double) async throws -> String {
         var bolusQuantity: Decimal = 0
         switch settingsManager.settings.bolusShortcut {

+ 59 - 17
Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -3,7 +3,7 @@ import Foundation
 import Intents
 import Swinject
 
-@available(iOS 16.0,*) struct AddCarbPresetIntent: AppIntent {
+struct AddCarbPresetIntent: AppIntent {
     // Title of the action in the Shortcuts app
     static var title: LocalizedStringResource = "Add carbs"
 
@@ -14,25 +14,25 @@ import Swinject
         title: "Quantity Carbs",
         description: "Quantity of carbs in g",
         controlStyle: .field,
-        inclusiveRange: (lowerBound: 0, upperBound: 200),
-        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of carbs did you eat?"))
-    ) var carbQuantity: Double?
+        inclusiveRange: (lowerBound: 0, upperBound: 300),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of carbs?"))
+    ) var carbQuantity: Int?
 
     @Parameter(
         title: "Quantity Fat",
         description: "Quantity of fat in g",
-        default: 0.0,
-        inclusiveRange: (0, 200),
-        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of fat did you eat?"))
-    ) var fatQuantity: Double
+        default: 0,
+        inclusiveRange: (0, 300),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of fat?"))
+    ) var fatQuantity: Int
 
     @Parameter(
         title: "Quantity Protein",
         description: "Quantity of Protein in g",
-        default: 0.0,
-        inclusiveRange: (0, 200),
-        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of protein did you eat?"))
-    ) var proteinQuantity: Double
+        default: 0,
+        inclusiveRange: (0, 300),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of protein?"))
+    ) var proteinQuantity: Int
 
     @Parameter(
         title: "Date",
@@ -46,7 +46,7 @@ import Swinject
     ) var note: String?
 
     @Parameter(
-        title: "Confirm Before logging",
+        title: "Confirm Before Logging",
         description: "If toggled, you will need to confirm before logging",
         default: true
     ) var confirmBeforeApplying: Bool
@@ -71,13 +71,46 @@ import Swinject
 
     @MainActor func perform() async throws -> some ProvidesDialog {
         do {
-            let quantityCarbs: Double
+            let quantityCarbs: Int
             if let cq = carbQuantity {
                 quantityCarbs = cq
             } else {
                 quantityCarbs = try await $carbQuantity.requestValue("How many grams of carbs?")
             }
 
+            let request = CarbPresetIntentRequest()
+            let maxCarbs = Int(truncating: request.settingsManager.settings.maxCarbs as NSDecimalNumber)
+            let maxFat = Int(truncating: request.settingsManager.settings.maxFat as NSDecimalNumber)
+            let maxProtein = Int(truncating: request.settingsManager.settings.maxProtein as NSDecimalNumber)
+
+            guard quantityCarbs <= maxCarbs else {
+                return .result(
+                    dialog: IntentDialog(
+                        stringLiteral: String(
+                            localized: "Logging Failed: Max Carbs = \(maxCarbs) g"
+                        )
+                    )
+                )
+            }
+            guard proteinQuantity <= maxProtein else {
+                return .result(
+                    dialog: IntentDialog(
+                        stringLiteral: String(
+                            localized: "Logging Failed: Max Protein = \(maxProtein) g"
+                        )
+                    )
+                )
+            }
+            guard fatQuantity <= maxFat else {
+                return .result(
+                    dialog: IntentDialog(
+                        stringLiteral: String(
+                            localized: "Logging Failed: Max Fat = \(maxFat) g"
+                        )
+                    )
+                )
+            }
+
             let dateCarbsAdded: Date
             let dateDefinedByUser: Bool
             if let da = dateAdded {
@@ -88,16 +121,25 @@ import Swinject
                 dateDefinedByUser = false
             }
 
-            let quantityCarbsName = quantityCarbs.toString()
             if confirmBeforeApplying {
+                var confirmationMessage: String
+                confirmationMessage = String(localized: "Add \(quantityCarbs) g carbs")
+                if fatQuantity > 0 {
+                    confirmationMessage = String(localized: "\(confirmationMessage) and \(fatQuantity) g fat")
+                }
+                if proteinQuantity > 0 {
+                    confirmationMessage = String(localized: "\(confirmationMessage) and \(proteinQuantity) g protein")
+                }
+                confirmationMessage = String(localized: "\(confirmationMessage)?")
+
                 try await requestConfirmation(
                     result: .result(
-                        dialog: IntentDialog(stringLiteral: String(localized: "Add \(quantityCarbsName) grams of carbs?"))
+                        dialog: IntentDialog(stringLiteral: confirmationMessage)
                     )
                 )
             }
 
-            let finalQuantityCarbsDisplay = try await CarbPresetIntentRequest().addCarbs(
+            let finalQuantityCarbsDisplay = try await request.addCarbs(
                 quantityCarbs,
                 fatQuantity,
                 proteinQuantity,

+ 12 - 14
Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -1,27 +1,25 @@
 import CoreData
 import Foundation
 
-@available(iOS 16.0,*) final class CarbPresetIntentRequest: BaseIntentsRequest {
+final class CarbPresetIntentRequest: BaseIntentsRequest {
     func addCarbs(
-        _ quantityCarbs: Double,
-        _ quantityFat: Double,
-        _ quantityProtein: Double,
+        _ quantityCarbs: Int,
+        _ quantityFat: Int,
+        _ quantityProtein: Int,
         _ dateAdded: Date,
         _ note: String?,
         _ dateDefinedByUser: Bool
     ) async throws -> String {
-        guard quantityCarbs >= 0.0 || quantityFat >= 0.0 || quantityProtein >= 0.0 else {
-            return "not adding carbs in Trio"
+        guard quantityCarbs >= 0 || quantityFat >= 0 || quantityProtein >= 0 else {
+            return "Amount must be positive."
         }
 
-        let carbs = min(Decimal(quantityCarbs), settingsManager.settings.maxCarbs)
-
         try await carbsStorage.storeCarbs(
             [CarbsEntry(
                 id: UUID().uuidString,
                 createdAt: dateAdded,
                 actualDate: dateAdded,
-                carbs: carbs,
+                carbs: Decimal(quantityCarbs),
                 fat: Decimal(quantityFat),
                 protein: Decimal(quantityProtein),
                 note: (note?.isEmpty ?? true) ? "Via Shortcut" : note!,
@@ -31,12 +29,12 @@ import Foundation
             areFetchedFromRemote: false
         )
         var resultDisplay: String
-        resultDisplay = String(localized: "Added \(String(format: "%.0f", Double(carbs))) g carbs")
-        if quantityFat > 0.0 {
-            resultDisplay = String(localized: "\(resultDisplay) and \(String(format: "%.0f", Double(quantityFat))) g fat")
+        resultDisplay = String(localized: "Added \(quantityCarbs) g carbs")
+        if quantityFat > 0 {
+            resultDisplay = String(localized: "\(resultDisplay) and \(quantityFat) g fat")
         }
-        if quantityProtein > 0.0 {
-            resultDisplay = String(localized: "\(resultDisplay) and \(String(format: "%.0f", Double(quantityProtein))) g protein")
+        if quantityProtein > 0 {
+            resultDisplay = String(localized: "\(resultDisplay) and \(quantityProtein) g protein")
         }
         if dateDefinedByUser {
             let dateFormatter = DateFormatter()

+ 59 - 0
TrioTests/SettingsExportTests.swift

@@ -0,0 +1,59 @@
+@testable import Trio
+import XCTest
+
+final class SettingsExportTests: XCTestCase {
+    func testCSVEscaping() {
+        // Test CSV escaping functionality
+        let testValue = "Test,Value\"With\nSpecial Characters"
+        let escaped = csvEscape(testValue)
+        let expected = "\"Test,Value\"\"With\nSpecial Characters\""
+        XCTAssertEqual(escaped, expected, "CSV escaping should handle commas, quotes, and newlines")
+    }
+
+    func testCSVEscapingSimple() {
+        // Test simple values don't get escaped
+        let testValue = "SimpleValue"
+        let escaped = csvEscape(testValue)
+        XCTAssertEqual(escaped, testValue, "Simple values should not be escaped")
+    }
+
+    func testExportCSVStructure() {
+        // Test that the CSV has the expected header structure
+        let expectedHeader = "Setting Category,Subcategory,Setting Name,Value,Unit"
+        // This test would require mocking the settings manager and file storage
+        // For now, we verify the header format is correct
+        XCTAssertEqual(expectedHeader.components(separatedBy: ",").count, 5, "CSV header should have 5 columns")
+    }
+
+    func testExportErrorTypes() {
+        // Test that our export error types are properly defined
+        let documentError = Settings.StateModel.ExportError.documentsDirectoryNotFound
+        XCTAssertNotNil(documentError.errorDescription, "Document error should have description")
+
+        let writeError = Settings.StateModel.ExportError.fileWriteError(TestError.testError)
+        XCTAssertNotNil(writeError.errorDescription, "Write error should have description")
+
+        let unknownError = Settings.StateModel.ExportError.unknown("Test message")
+        XCTAssertNotNil(unknownError.errorDescription, "Unknown error should have description")
+    }
+
+    func testExportFileNaming() {
+        // Test that export files have the correct naming pattern
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyyMMdd_HHmmss"
+        let timestamp = formatter.string(from: Date())
+        let fileName = "TrioSettings_\(timestamp).csv"
+
+        XCTAssertTrue(fileName.hasPrefix("TrioSettings_"), "File name should start with TrioSettings_")
+        XCTAssertTrue(fileName.hasSuffix(".csv"), "File name should end with .csv")
+        XCTAssertEqual(fileName.components(separatedBy: "_").count, 2, "File name should have one underscore")
+    }
+
+    // Helper function to test CSV escaping (extracted from Settings.StateModel)
+    private func csvEscape(_ value: String) -> String {
+        if value.contains(",") || value.contains("\"") || value.contains("\n") {
+            return "\"\(value.replacingOccurrences(of: "\"", with: "\"\""))\""
+        }
+        return value
+    }
+}