SettingsExportStateModel.swift 67 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330
  1. import CoreData
  2. import Foundation
  3. import LoopKit
  4. import SwiftUI
  5. import Swinject
  6. extension SettingsExport {
  7. final class StateModel: BaseStateModel<Provider> {
  8. @Injected() private var broadcaster: Broadcaster!
  9. @Injected() private var fileManager: FileManager!
  10. @Injected() private var storage: FileStorage!
  11. @Injected() var overrideStorage: OverrideStorage!
  12. @Injected() var tempTargetsStorage: TempTargetsStorage!
  13. // Help Sheet
  14. var isHelpSheetPresented: Bool = false
  15. var helpSheetDetent = PresentationDetent.large
  16. // Version information
  17. private var versionNumber: String = ""
  18. private var buildNumber: String = ""
  19. private var branch: String = ""
  20. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  21. override func subscribe() {
  22. versionNumber = Bundle.main.appDevVersion ?? "Unknown"
  23. buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
  24. branch = BuildDetails.shared.branchAndSha
  25. }
  26. // Export categories for selective export
  27. enum ExportCategory: String, CaseIterable, Identifiable {
  28. var id: String { rawValue }
  29. case metadata = "Metadata"
  30. case devices = "Devices"
  31. case therapy = "Therapy"
  32. case algorithm = "Algorithm"
  33. case features = "Features"
  34. case notifications = "Notifications"
  35. case services = "Services"
  36. case tempTargetPresets = "Temp Target Presets"
  37. case overridePresets = "Override Presets"
  38. case mealPresets = "Meal Presets"
  39. }
  40. // Published state for UI binding
  41. @Published var selectedCategories: Set<ExportCategory> = Set(ExportCategory.allCases)
  42. @Published var isExporting: Bool = false
  43. enum ExportError: LocalizedError {
  44. case documentsDirectoryNotFound
  45. case fileWriteError(Error)
  46. case unknown(String)
  47. var errorDescription: String? {
  48. switch self {
  49. case .documentsDirectoryNotFound:
  50. return String(localized: "Could not access documents directory")
  51. case let .fileWriteError(error):
  52. return String(localized: "Failed to write export file: \(error.localizedDescription)")
  53. case let .unknown(message):
  54. return String(localized: "Export failed: \(message)")
  55. }
  56. }
  57. }
  58. /// Exports selected Trio settings to a CSV file
  59. ///
  60. /// This function creates an export of the user's selected Trio configuration categories including:
  61. /// - Export metadata (date, app version, build) [optional]
  62. /// - Device settings (CGM, pump information) [optional]
  63. /// - Therapy profiles (basal rates, ISF, carb ratios, targets) [optional]
  64. /// - Algorithm settings (SMB, autosens, dynamic settings, etc.) [optional]
  65. /// - Features and UI preferences [optional]
  66. /// - Notification settings [optional]
  67. /// - Service configurations [optional]
  68. /// - Preset data [optional]
  69. ///
  70. /// - Parameter categories: Set of categories to include in export. If nil, exports all categories.
  71. /// - Parameter format: Export format to use. If nil, uses currently selected format.
  72. /// - Returns: A Result containing either the file URL on success or an ExportError on failure
  73. func exportSettings(
  74. categories: Set<ExportCategory>? = nil
  75. ) async -> Result<URL, ExportError> {
  76. debug(.default, "🔄 EXPORT: Starting settings export...")
  77. await MainActor.run { isExporting = true }
  78. defer { Task { @MainActor in isExporting = false } }
  79. let categoriesToExport = categories ?? selectedCategories
  80. debug(
  81. .default,
  82. "🔄 EXPORT: Exporting categories: \(categoriesToExport.map(\.rawValue).joined(separator: ", ")) in .CSV format"
  83. )
  84. let formatter = DateFormatter()
  85. formatter.dateFormat = "yyyyMMdd_HHmmss"
  86. let timestamp = formatter.string(from: Date())
  87. let fileName = "TrioSettings_\(timestamp).csv"
  88. // Use the Documents directory for better sharing compatibility
  89. guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
  90. return .failure(.documentsDirectoryNotFound)
  91. }
  92. let fileURL = documentsDirectory.appendingPathComponent(fileName)
  93. debug(.default, "Export file path: \(fileURL.path)")
  94. var exportSettings: [ExportSetting] = []
  95. let trioSettings = settingsManager.settings
  96. let preferences = settingsManager.preferences
  97. debug(.default, "🔄 EXPORT: Settings managers initialized")
  98. // Helper function to add a setting
  99. func addSetting(category: String, subcategory: String = "", name: String, value: String, unit: String = "") {
  100. exportSettings.append(ExportSetting(
  101. category: category,
  102. subcategory: subcategory,
  103. name: name,
  104. value: value,
  105. unit: unit
  106. ))
  107. }
  108. // Export metadata - always include basic export info
  109. if categoriesToExport.contains(.metadata) {
  110. let exportCategory = String(localized: "Metadata")
  111. addSetting(
  112. category: exportCategory,
  113. name: String(localized: "Export Date"),
  114. value: DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium)
  115. )
  116. addSetting(category: exportCategory, name: String(localized: "App Version"), value: versionNumber)
  117. addSetting(category: exportCategory, name: String(localized: "Build Number"), value: buildNumber)
  118. addSetting(category: exportCategory, name: String(localized: "Branch"), value: branch)
  119. }
  120. // Devices
  121. if categoriesToExport.contains(.devices) {
  122. let devicesCategory = String(localized: "Devices", comment: "Devices menu item in the Settings main view.")
  123. addSetting(category: devicesCategory, name: String(localized: "CGM"), value: trioSettings.cgm.rawValue)
  124. addSetting(
  125. category: devicesCategory,
  126. name: String(localized: "Smooth Glucose Value"),
  127. value: trioSettings.smoothGlucose ? String(localized: "Enabled") : String(localized: "Disabled")
  128. )
  129. // Pump Information
  130. if let pumpManager = provider.deviceManager.pumpManager {
  131. addSetting(category: devicesCategory, name: String(localized: "Pump Type"), value: pumpManager.localizedTitle)
  132. // Get insulin type from pump manager if available, otherwise from preferences
  133. let insulinTypeValue: String
  134. if let pumpManager = provider.deviceManager.pumpManager,
  135. let insulinType = pumpManager.status.insulinType
  136. {
  137. insulinTypeValue = insulinType.title
  138. } else {
  139. insulinTypeValue = preferences.curve.rawValue
  140. // technically, this gets set only when a pump is onboared
  141. // leaving this here as a backup, because you theoretically could
  142. // have removed your PM instance, but are just within pumps and
  143. // insulin type stays the same.
  144. // however, this theoretically could be a stale type.
  145. }
  146. addSetting(
  147. category: devicesCategory,
  148. name: String(localized: "Insulin Type"),
  149. value: insulinTypeValue
  150. )
  151. } else {
  152. addSetting(
  153. category: devicesCategory,
  154. name: String(localized: "Pump Type"),
  155. value: String(localized: "Not Connected")
  156. )
  157. }
  158. }
  159. // Therapy Settings
  160. if categoriesToExport.contains(.therapy) {
  161. let therapyCategory = String(localized: "Therapy", comment: "Therapy menu item in the Settings main view.")
  162. // Units and Limits subcategory
  163. let unitsLimitsSubcategory = String(localized: "Units and Limits")
  164. addSetting(
  165. category: therapyCategory,
  166. subcategory: unitsLimitsSubcategory,
  167. name: String(localized: "Glucose Units"),
  168. value: trioSettings.units.rawValue
  169. )
  170. addSetting(
  171. category: therapyCategory,
  172. subcategory: unitsLimitsSubcategory,
  173. name: String(localized: "Maximum Insulin on Board (IOB)"),
  174. value: String(describing: preferences.maxIOB),
  175. unit: "U"
  176. )
  177. // Add missing pump settings from PumpSettings
  178. let pumpSettings = settingsManager.pumpSettings
  179. addSetting(
  180. category: therapyCategory,
  181. subcategory: unitsLimitsSubcategory,
  182. name: String(localized: "Maximum Bolus"),
  183. value: String(describing: pumpSettings.maxBolus),
  184. unit: "U"
  185. )
  186. addSetting(
  187. category: therapyCategory,
  188. subcategory: unitsLimitsSubcategory,
  189. name: String(localized: "Maximum Basal Rate"),
  190. value: String(describing: pumpSettings.maxBasal),
  191. unit: String(localized: "U/hr", comment: "Insulin unit per hour abbreviation")
  192. )
  193. addSetting(
  194. category: therapyCategory,
  195. subcategory: unitsLimitsSubcategory,
  196. name: String(localized: "Maximum Carbs on Board (COB)"),
  197. value: String(describing: preferences.maxCOB),
  198. unit: String(localized: "g", comment: "Units for carbs")
  199. )
  200. addSetting(
  201. category: therapyCategory,
  202. subcategory: unitsLimitsSubcategory,
  203. name: String(localized: "Minimum Safety Threshold"),
  204. value: trioSettings
  205. .units == .mgdL ? String(describing: preferences.threshold_setting) :
  206. String(describing: preferences.threshold_setting.asMmolL),
  207. unit: trioSettings.units.rawValue
  208. )
  209. // Get therapy profiles from storage
  210. let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) ?? []
  211. let isfProfileContainer = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
  212. let crProfileContainer = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
  213. let targetProfileContainer = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  214. // Glucose Targets subcategory
  215. let glucoseTargetsSubcategory = String(localized: "Glucose Targets")
  216. if let targetContainer = targetProfileContainer {
  217. for entry in targetContainer.targets {
  218. // Export single target value since high==low in Trio
  219. let targetValue = trioSettings.units == .mgdL ? entry.low : entry.low.asMmolL
  220. addSetting(
  221. category: therapyCategory,
  222. subcategory: glucoseTargetsSubcategory,
  223. name: String(localized: "Target (\(entry.start.formattedHourMinuteFromTimeString()))"),
  224. value: String(describing: targetValue),
  225. unit: trioSettings.units.rawValue
  226. )
  227. }
  228. }
  229. // Basal Rates subcategory
  230. let basalRatesSubcategory = String(localized: "Basal Rates")
  231. for entry in basalProfile {
  232. addSetting(
  233. category: therapyCategory,
  234. subcategory: basalRatesSubcategory,
  235. name: String(localized: "Basal Rate (\(entry.start.formattedHourMinuteFromTimeString()))"),
  236. value: String(describing: entry.rate),
  237. unit: String(localized: "U/hr", comment: "Insulin unit per hour abbreviation")
  238. )
  239. }
  240. // Carb Ratios subcategory
  241. let carbRatiosSubcategory = String(localized: "Carb Ratios")
  242. if let crContainer = crProfileContainer {
  243. for entry in crContainer.schedule {
  244. addSetting(
  245. category: therapyCategory,
  246. subcategory: carbRatiosSubcategory,
  247. name: String(localized: "Carb Ratio (\(entry.start.formattedHourMinuteFromTimeString()))"),
  248. value: String(describing: entry.ratio),
  249. unit: String(localized: "g/U")
  250. )
  251. }
  252. }
  253. // Insulin Sensitivities subcategory
  254. let insulinSensitivitiesSubcategory = String(localized: "Insulin Sensitivities")
  255. if let isfContainer = isfProfileContainer {
  256. for entry in isfContainer.sensitivities {
  257. let isfValue = trioSettings.units == .mgdL ? entry.sensitivity : entry.sensitivity.asMmolL
  258. addSetting(
  259. category: therapyCategory,
  260. subcategory: insulinSensitivitiesSubcategory,
  261. name: String(localized: "ISF (\(entry.start.formattedHourMinuteFromTimeString()))"),
  262. value: String(describing: isfValue),
  263. unit: trioSettings.units.rawValue
  264. )
  265. }
  266. }
  267. }
  268. // Algorithm Settings
  269. if categoriesToExport.contains(.algorithm) {
  270. let algorithmCategory = String(localized: "Algorithm", comment: "Algorithm menu item in the Settings main view.")
  271. let pumpSettings = settingsManager.pumpSettings
  272. // Autosens Settings
  273. let autosensSubcategory = String(localized: "Autosens")
  274. addSetting(
  275. category: algorithmCategory,
  276. subcategory: autosensSubcategory,
  277. name: String(localized: "Autosens Max"),
  278. value: String(format: "%.0f", (preferences.autosensMax as NSDecimalNumber).doubleValue * 100),
  279. unit: "%"
  280. )
  281. addSetting(
  282. category: algorithmCategory,
  283. subcategory: autosensSubcategory,
  284. name: String(localized: "Autosens Min"),
  285. value: String(format: "%.0f", (preferences.autosensMin as NSDecimalNumber).doubleValue * 100),
  286. unit: "%"
  287. )
  288. addSetting(
  289. category: algorithmCategory,
  290. subcategory: autosensSubcategory,
  291. name: String(localized: "Rewind Resets Autosens"),
  292. value: preferences.rewindResetsAutosens ? String(localized: "Enabled") : String(localized: "Disabled")
  293. )
  294. // SMB Settings
  295. let smbSubcategory = String(localized: "SMB")
  296. addSetting(
  297. category: algorithmCategory,
  298. subcategory: smbSubcategory,
  299. name: String(localized: "Enable SMB Always"),
  300. value: preferences.enableSMBAlways ? String(localized: "Enabled") : String(localized: "Disabled")
  301. )
  302. addSetting(
  303. category: algorithmCategory,
  304. subcategory: smbSubcategory,
  305. name: String(localized: "Enable SMB With COB"),
  306. value: preferences.enableSMBWithCOB ? String(localized: "Enabled") : String(localized: "Disabled")
  307. )
  308. addSetting(
  309. category: algorithmCategory,
  310. subcategory: smbSubcategory,
  311. name: String(localized: "Enable SMB With Temptarget"),
  312. value: preferences.enableSMBWithTemptarget ? String(localized: "Enabled") : String(localized: "Disabled")
  313. )
  314. addSetting(
  315. category: algorithmCategory,
  316. subcategory: smbSubcategory,
  317. name: String(localized: "Enable SMB After Carbs"),
  318. value: preferences.enableSMBAfterCarbs ? String(localized: "Enabled") : String(localized: "Disabled")
  319. )
  320. addSetting(
  321. category: algorithmCategory,
  322. subcategory: smbSubcategory,
  323. name: String(localized: "Enable SMB With High Glucose"),
  324. value: preferences.enableSMB_high_bg ? String(localized: "Enabled") : String(localized: "Disabled")
  325. )
  326. if preferences.enableSMB_high_bg {
  327. addSetting(
  328. category: algorithmCategory,
  329. subcategory: smbSubcategory,
  330. name: String(localized: "High Glucose Target"),
  331. value: trioSettings
  332. .units == .mgdL ? String(describing: preferences.enableSMB_high_bg_target) :
  333. String(describing: preferences.enableSMB_high_bg_target.asMmolL),
  334. unit: trioSettings.units.rawValue
  335. )
  336. }
  337. addSetting(
  338. category: algorithmCategory,
  339. subcategory: smbSubcategory,
  340. name: String(localized: "Allow SMB With High Temptarget"),
  341. value: preferences.allowSMBWithHighTemptarget ? String(localized: "Enabled") : String(localized: "Disabled")
  342. )
  343. addSetting(
  344. category: algorithmCategory,
  345. subcategory: smbSubcategory,
  346. name: String(localized: "Enable UAM"),
  347. value: preferences.enableUAM ? String(localized: "Enabled") : String(localized: "Disabled")
  348. )
  349. addSetting(
  350. category: algorithmCategory,
  351. subcategory: smbSubcategory,
  352. name: String(localized: "Max SMB Basal Minutes"),
  353. value: String(describing: preferences.maxSMBBasalMinutes),
  354. unit: String(localized: "minutes")
  355. )
  356. addSetting(
  357. category: algorithmCategory,
  358. subcategory: smbSubcategory,
  359. name: String(localized: "Max UAM Basal Minutes"),
  360. value: String(describing: preferences.maxUAMSMBBasalMinutes),
  361. unit: String(localized: "minutes")
  362. )
  363. addSetting(
  364. category: algorithmCategory,
  365. subcategory: smbSubcategory,
  366. name: String(localized: "Max Allowed Glucose Rise for SMB"),
  367. value: String(format: "%.0f", (preferences.maxDeltaBGthreshold as NSDecimalNumber).doubleValue * 100),
  368. unit: "%"
  369. )
  370. // Dynamic Settings
  371. let dynamicSubcategory = String(localized: "Dynamic Settings")
  372. // Proper Dynamic ISF handling using the current enum logic
  373. let dynamicISFValue: String
  374. if !preferences.useNewFormula {
  375. dynamicISFValue = String(localized: "Disabled")
  376. } else if preferences.sigmoid {
  377. dynamicISFValue = String(localized: "Sigmoid")
  378. } else {
  379. dynamicISFValue = String(localized: "Logarithmic")
  380. }
  381. addSetting(
  382. category: algorithmCategory,
  383. subcategory: dynamicSubcategory,
  384. name: String(localized: "Dynamic ISF"),
  385. value: dynamicISFValue
  386. )
  387. // Show adjustment factors as percentages with proper labels
  388. if preferences.useNewFormula {
  389. if !preferences.sigmoid {
  390. addSetting(
  391. category: algorithmCategory,
  392. subcategory: dynamicSubcategory,
  393. name: String(localized: "Adjustment Factor (AF)"),
  394. value: String(format: "%.0f", (preferences.adjustmentFactor as NSDecimalNumber).doubleValue * 100),
  395. unit: "%"
  396. )
  397. } else {
  398. addSetting(
  399. category: algorithmCategory,
  400. subcategory: dynamicSubcategory,
  401. name: String(localized: "Sigmoid Adjustment Factor"),
  402. value: String(
  403. format: "%.0f",
  404. (preferences.adjustmentFactorSigmoid as NSDecimalNumber).doubleValue * 100
  405. ),
  406. unit: "%"
  407. )
  408. }
  409. }
  410. // Weighted Average of TDD is shown for both logarithmic and sigmoid when Dynamic ISF is enabled
  411. addSetting(
  412. category: algorithmCategory,
  413. subcategory: dynamicSubcategory,
  414. name: String(localized: "Weighted Average of TDD"),
  415. value: String(format: "%.0f", (preferences.weightPercentage as NSDecimalNumber).doubleValue * 100),
  416. unit: "%"
  417. )
  418. addSetting(
  419. category: algorithmCategory,
  420. subcategory: dynamicSubcategory,
  421. name: String(localized: "Adjust Basal"),
  422. value: preferences.tddAdjBasal ? String(localized: "Enabled") : String(localized: "Disabled")
  423. )
  424. // Target Behavior
  425. let targetBehaviorSubcategory = String(localized: "Target Behavior")
  426. addSetting(
  427. category: algorithmCategory,
  428. subcategory: targetBehaviorSubcategory,
  429. name: String(localized: "High Temptarget Raises Sensitivity"),
  430. value: preferences
  431. .highTemptargetRaisesSensitivity ? String(localized: "Enabled") : String(localized: "Disabled")
  432. )
  433. addSetting(
  434. category: algorithmCategory,
  435. subcategory: targetBehaviorSubcategory,
  436. name: String(localized: "Low Temptarget Lowers Sensitivity"),
  437. value: preferences
  438. .lowTemptargetLowersSensitivity ? String(localized: "Enabled") : String(localized: "Disabled")
  439. )
  440. addSetting(
  441. category: algorithmCategory,
  442. subcategory: targetBehaviorSubcategory,
  443. name: String(localized: "Sensitivity Raises Target"),
  444. value: preferences.sensitivityRaisesTarget ? String(localized: "Enabled") : String(localized: "Disabled")
  445. )
  446. addSetting(
  447. category: algorithmCategory,
  448. subcategory: targetBehaviorSubcategory,
  449. name: String(localized: "Resistance Lowers Target"),
  450. value: preferences.resistanceLowersTarget ? String(localized: "Enabled") : String(localized: "Disabled")
  451. )
  452. addSetting(
  453. category: algorithmCategory,
  454. subcategory: targetBehaviorSubcategory,
  455. name: String(localized: "Half Basal Exercise Target"),
  456. value: trioSettings
  457. .units == .mgdL ? String(describing: preferences.halfBasalExerciseTarget) :
  458. String(describing: preferences.halfBasalExerciseTarget.asMmolL),
  459. unit: trioSettings.units.rawValue
  460. )
  461. // Additional Algorithm Settings
  462. let additionalsSubcategory = String(localized: "Additionals")
  463. addSetting(
  464. category: algorithmCategory,
  465. subcategory: additionalsSubcategory,
  466. name: String(localized: "Max Daily Safety Multiplier"),
  467. value: String(format: "%.0f", (preferences.maxDailySafetyMultiplier as NSDecimalNumber).doubleValue * 100),
  468. unit: "%"
  469. )
  470. addSetting(
  471. category: algorithmCategory,
  472. subcategory: additionalsSubcategory,
  473. name: String(localized: "Current Basal Safety Multiplier"),
  474. value: String(
  475. format: "%.0f",
  476. (preferences.currentBasalSafetyMultiplier as NSDecimalNumber).doubleValue * 100
  477. ),
  478. unit: "%"
  479. )
  480. addSetting(
  481. category: algorithmCategory,
  482. subcategory: additionalsSubcategory,
  483. name: String(localized: "Use Custom Peak Time"),
  484. value: preferences.useCustomPeakTime ? String(localized: "Enabled") : String(localized: "Disabled")
  485. )
  486. addSetting(
  487. category: algorithmCategory,
  488. subcategory: additionalsSubcategory,
  489. name: String(localized: "Duration of Insulin Action"),
  490. value: String(describing: pumpSettings.insulinActionCurve),
  491. unit: String(localized: "hours")
  492. )
  493. addSetting(
  494. category: algorithmCategory,
  495. subcategory: additionalsSubcategory,
  496. name: String(localized: "Insulin Peak Time"),
  497. value: String(describing: preferences.insulinPeakTime),
  498. unit: String(localized: "minutes")
  499. )
  500. addSetting(
  501. category: algorithmCategory,
  502. subcategory: additionalsSubcategory,
  503. name: String(localized: "Skip Neutral Temps"),
  504. value: preferences.skipNeutralTemps ? String(localized: "Enabled") : String(localized: "Disabled")
  505. )
  506. addSetting(
  507. category: algorithmCategory,
  508. subcategory: additionalsSubcategory,
  509. name: String(localized: "Unsuspend If No Temp"),
  510. value: preferences.unsuspendIfNoTemp ? String(localized: "Enabled") : String(localized: "Disabled")
  511. )
  512. addSetting(
  513. category: algorithmCategory,
  514. subcategory: additionalsSubcategory,
  515. name: String(localized: "Suspend Zeros IOB"),
  516. value: preferences.suspendZerosIOB ? String(localized: "Enabled") : String(localized: "Disabled")
  517. )
  518. // SMB settings that belong in Additionals (correct order based on UI)
  519. addSetting(
  520. category: algorithmCategory,
  521. subcategory: additionalsSubcategory,
  522. name: String(localized: "SMB Delivery Ratio"),
  523. value: String(format: "%.0f", (preferences.smbDeliveryRatio as NSDecimalNumber).doubleValue * 100),
  524. unit: "%"
  525. )
  526. addSetting(
  527. category: algorithmCategory,
  528. subcategory: additionalsSubcategory,
  529. name: String(localized: "SMB Interval"),
  530. value: String(describing: preferences.smbInterval),
  531. unit: String(localized: "minutes")
  532. )
  533. addSetting(
  534. category: algorithmCategory,
  535. subcategory: additionalsSubcategory,
  536. name: String(localized: "Min 5m Carb Impact"),
  537. value: trioSettings
  538. .units == .mgdL ? String(describing: preferences.min5mCarbimpact) :
  539. String(describing: preferences.min5mCarbimpact.asMmolL),
  540. unit: trioSettings.units.rawValue
  541. )
  542. addSetting(
  543. category: algorithmCategory,
  544. subcategory: additionalsSubcategory,
  545. name: String(localized: "Remaining Carbs Percentage"),
  546. value: String(format: "%.0f", (preferences.remainingCarbsFraction as NSDecimalNumber).doubleValue * 100),
  547. unit: "%"
  548. )
  549. addSetting(
  550. category: algorithmCategory,
  551. subcategory: additionalsSubcategory,
  552. name: String(localized: "Remaining Carbs Cap"),
  553. value: String(describing: preferences.remainingCarbsCap),
  554. unit: String(localized: "g", comment: "Units for carbs")
  555. )
  556. addSetting(
  557. category: algorithmCategory,
  558. subcategory: additionalsSubcategory,
  559. name: String(localized: "Noisy CGM Target Increase"),
  560. value: String(format: "%.0f", (preferences.noisyCGMTargetMultiplier as NSDecimalNumber).doubleValue * 100),
  561. unit: "%"
  562. )
  563. }
  564. // Features
  565. if categoriesToExport.contains(.features) {
  566. let featuresCategory = String(localized: "Features", comment: "Features menu item in the Settings main view.")
  567. // Trio Features subcategory - Bolus Calculator
  568. let bolusCalculatorSubcategory = String(localized: "Bolus Calculator")
  569. addSetting(
  570. category: featuresCategory,
  571. subcategory: bolusCalculatorSubcategory,
  572. name: String(localized: "Display Meal Presets"),
  573. value: trioSettings.displayPresets ? String(localized: "Enabled") : String(localized: "Disabled")
  574. )
  575. addSetting(
  576. category: featuresCategory,
  577. subcategory: bolusCalculatorSubcategory,
  578. name: String(localized: "Recommended Bolus Percentage"),
  579. value: String(format: "%.0f", (trioSettings.overrideFactor as NSDecimalNumber).doubleValue * 100),
  580. unit: "%"
  581. )
  582. addSetting(
  583. category: featuresCategory,
  584. subcategory: bolusCalculatorSubcategory,
  585. name: String(localized: "Enable Reduced Bolus Option"),
  586. value: trioSettings.fattyMeals ? String(localized: "Enabled") : String(localized: "Disabled")
  587. )
  588. if trioSettings.fattyMeals {
  589. addSetting(
  590. category: featuresCategory,
  591. subcategory: bolusCalculatorSubcategory,
  592. name: String(localized: "Reduced Bolus Percentage"),
  593. value: String(format: "%.0f", (trioSettings.fattyMealFactor as NSDecimalNumber).doubleValue * 100),
  594. unit: "%"
  595. )
  596. }
  597. addSetting(
  598. category: featuresCategory,
  599. subcategory: bolusCalculatorSubcategory,
  600. name: String(localized: "Enable Super Bolus Option"),
  601. value: trioSettings.sweetMeals ? String(localized: "Enabled") : String(localized: "Disabled")
  602. )
  603. if trioSettings.sweetMeals {
  604. addSetting(
  605. category: featuresCategory,
  606. subcategory: bolusCalculatorSubcategory,
  607. name: String(localized: "Super Bolus Percentage"),
  608. value: String(format: "%.0f", (trioSettings.sweetMealFactor as NSDecimalNumber).doubleValue * 100),
  609. unit: "%"
  610. )
  611. }
  612. addSetting(
  613. category: featuresCategory,
  614. subcategory: bolusCalculatorSubcategory,
  615. name: String(localized: "Very Low Glucose Warning"),
  616. value: trioSettings.confirmBolus ? String(localized: "Enabled") : String(localized: "Disabled")
  617. )
  618. // Trio Features subcategory - Meal Settings
  619. let mealSettingsSubcategory = String(localized: "Meal Settings")
  620. addSetting(
  621. category: featuresCategory,
  622. subcategory: mealSettingsSubcategory,
  623. name: String(localized: "Max Carbs"),
  624. value: String(describing: trioSettings.maxCarbs),
  625. unit: String(localized: "g", comment: "Units for carbs")
  626. )
  627. addSetting(
  628. category: featuresCategory,
  629. subcategory: mealSettingsSubcategory,
  630. name: String(localized: "Max Fat"),
  631. value: String(describing: trioSettings.maxFat),
  632. unit: String(localized: "g", comment: "Units for carbs")
  633. )
  634. addSetting(
  635. category: featuresCategory,
  636. subcategory: mealSettingsSubcategory,
  637. name: String(localized: "Max Protein"),
  638. value: String(describing: trioSettings.maxProtein),
  639. unit: String(localized: "g", comment: "Units for carbs")
  640. )
  641. addSetting(
  642. category: featuresCategory,
  643. subcategory: mealSettingsSubcategory,
  644. name: String(localized: "Max Meal Absorption Time"),
  645. value: String(describing: preferences.maxMealAbsorptionTime),
  646. unit: String(localized: "hours")
  647. )
  648. addSetting(
  649. category: featuresCategory,
  650. subcategory: mealSettingsSubcategory,
  651. name: String(localized: "Enable Fat and Protein Entries"),
  652. value: trioSettings.useFPUconversion ? String(localized: "Enabled") : String(localized: "Disabled")
  653. )
  654. addSetting(
  655. category: featuresCategory,
  656. subcategory: mealSettingsSubcategory,
  657. name: String(localized: "Fat and Protein Delay"),
  658. value: String(describing: trioSettings.delay),
  659. unit: String(localized: "minutes")
  660. )
  661. addSetting(
  662. category: featuresCategory,
  663. subcategory: mealSettingsSubcategory,
  664. name: String(localized: "Spread Interval"),
  665. value: String(describing: trioSettings.minuteInterval),
  666. unit: String(localized: "minutes")
  667. )
  668. addSetting(
  669. category: featuresCategory,
  670. subcategory: mealSettingsSubcategory,
  671. name: String(localized: "Fat and Protein Percentage"),
  672. value: String(format: "%.0f", (trioSettings.individualAdjustmentFactor as NSDecimalNumber).doubleValue * 100),
  673. unit: "%"
  674. )
  675. // Trio Features subcategory - Shortcuts
  676. let shortcutsSubcategory = String(localized: "Shortcuts")
  677. addSetting(
  678. category: featuresCategory,
  679. subcategory: shortcutsSubcategory,
  680. name: String(localized: "Allow Bolusing with Shortcuts"),
  681. value: trioSettings
  682. .bolusShortcut != .notAllowed ? String(localized: "Enabled") : String(localized: "Disabled")
  683. )
  684. // Trio Features subcategory - Remote Control
  685. let remoteControlSubcategory = String(localized: "Remote Control")
  686. let isRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
  687. addSetting(
  688. category: featuresCategory,
  689. subcategory: remoteControlSubcategory,
  690. name: String(localized: "Enable Remote Control"),
  691. value: isRemoteControlEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
  692. )
  693. // Trio Personalization subcategory - User Interface
  694. let userInterfaceSubcategory = String(localized: "User Interface")
  695. addSetting(
  696. category: featuresCategory,
  697. subcategory: userInterfaceSubcategory,
  698. name: String(localized: "Show X-Axis Grid Lines"),
  699. value: trioSettings.xGridLines ? String(localized: "Enabled") : String(localized: "Disabled")
  700. )
  701. addSetting(
  702. category: featuresCategory,
  703. subcategory: userInterfaceSubcategory,
  704. name: String(localized: "Show Y-Axis Grid Lines"),
  705. value: trioSettings.yGridLines ? String(localized: "Enabled") : String(localized: "Disabled")
  706. )
  707. addSetting(
  708. category: featuresCategory,
  709. subcategory: userInterfaceSubcategory,
  710. name: String(localized: "Show Low and High Thresholds"),
  711. value: trioSettings.rulerMarks ? String(localized: "Enabled") : String(localized: "Disabled")
  712. )
  713. addSetting(
  714. category: featuresCategory,
  715. subcategory: userInterfaceSubcategory,
  716. name: String(localized: "Low Threshold"),
  717. value: trioSettings
  718. .units == .mgdL ? String(describing: trioSettings.low) : String(describing: trioSettings.low.asMmolL),
  719. unit: trioSettings.units.rawValue
  720. )
  721. addSetting(
  722. category: featuresCategory,
  723. subcategory: userInterfaceSubcategory,
  724. name: String(localized: "High Threshold"),
  725. value: trioSettings
  726. .units == .mgdL ? String(describing: trioSettings.high) : String(describing: trioSettings.high.asMmolL),
  727. unit: trioSettings.units.rawValue
  728. )
  729. addSetting(
  730. category: featuresCategory,
  731. subcategory: userInterfaceSubcategory,
  732. name: String(localized: "eA1c/GMI Display Unit"),
  733. value: trioSettings.eA1cDisplayUnit.rawValue
  734. )
  735. addSetting(
  736. category: featuresCategory,
  737. subcategory: userInterfaceSubcategory,
  738. name: String(localized: "Show Carbs Required Badge"),
  739. value: trioSettings.showCarbsRequiredBadge ? String(localized: "Enabled") : String(localized: "Disabled")
  740. )
  741. addSetting(
  742. category: featuresCategory,
  743. subcategory: userInterfaceSubcategory,
  744. name: String(localized: "Carbs Required Threshold"),
  745. value: String(describing: trioSettings.carbsRequiredThreshold),
  746. unit: String(localized: "g", comment: "Units for carbs")
  747. )
  748. addSetting(
  749. category: featuresCategory,
  750. subcategory: userInterfaceSubcategory,
  751. name: String(localized: "Forecast Display Type"),
  752. value: trioSettings.forecastDisplayType.rawValue
  753. )
  754. addSetting(
  755. category: featuresCategory,
  756. subcategory: userInterfaceSubcategory,
  757. name: String(localized: "Glucose Color Scheme"),
  758. value: trioSettings.glucoseColorScheme.rawValue
  759. )
  760. addSetting(
  761. category: featuresCategory,
  762. subcategory: userInterfaceSubcategory,
  763. name: String(localized: "Time in Range Type"),
  764. value: trioSettings.timeInRangeType.rawValue
  765. )
  766. addSetting(
  767. category: featuresCategory,
  768. subcategory: userInterfaceSubcategory,
  769. name: String(localized: "Require Adjustments Confirmation"),
  770. value: trioSettings
  771. .requireAdjustmentsConfirmation ? String(localized: "Enabled") : String(localized: "Disabled")
  772. )
  773. // Appearance setting from UserDefaults
  774. let colorSchemePreference = UserDefaults.standard.string(forKey: "colorSchemePreference") ?? "systemDefault"
  775. let appearanceValue: String
  776. switch colorSchemePreference {
  777. case "systemDefault":
  778. appearanceValue = String(localized: "System Default")
  779. case "light":
  780. appearanceValue = String(localized: "Light")
  781. case "dark":
  782. appearanceValue = String(localized: "Dark")
  783. default:
  784. appearanceValue = String(localized: "System Default")
  785. }
  786. addSetting(
  787. category: featuresCategory,
  788. subcategory: userInterfaceSubcategory,
  789. name: String(localized: "Appearance"),
  790. value: appearanceValue
  791. )
  792. }
  793. // Notifications
  794. if categoriesToExport.contains(.notifications) {
  795. let notificationsCategory = String(
  796. localized: "Notifications",
  797. comment: "Notifications menu item in the Settings main view."
  798. )
  799. // Trio Notifications subcategory
  800. let trioNotificationsSubcategory = String(localized: "Trio Notifications")
  801. addSetting(
  802. category: notificationsCategory,
  803. subcategory: trioNotificationsSubcategory,
  804. name: String(localized: "Always Notify Pump"),
  805. value: trioSettings.notificationsPump ? String(localized: "Enabled") : String(localized: "Disabled")
  806. )
  807. addSetting(
  808. category: notificationsCategory,
  809. subcategory: trioNotificationsSubcategory,
  810. name: String(localized: "Always Notify CGM"),
  811. value: trioSettings.notificationsCgm ? String(localized: "Enabled") : String(localized: "Disabled")
  812. )
  813. addSetting(
  814. category: notificationsCategory,
  815. subcategory: trioNotificationsSubcategory,
  816. name: String(localized: "Always Notify Carb"),
  817. value: trioSettings.notificationsCarb ? String(localized: "Enabled") : String(localized: "Disabled")
  818. )
  819. addSetting(
  820. category: notificationsCategory,
  821. subcategory: trioNotificationsSubcategory,
  822. name: String(localized: "Always Notify Algorithm"),
  823. value: trioSettings.notificationsAlgorithm ? String(localized: "Enabled") : String(localized: "Disabled")
  824. )
  825. addSetting(
  826. category: notificationsCategory,
  827. subcategory: trioNotificationsSubcategory,
  828. name: String(localized: "Show Glucose App Badge"),
  829. value: trioSettings.glucoseBadge ? String(localized: "Enabled") : String(localized: "Disabled")
  830. )
  831. addSetting(
  832. category: notificationsCategory,
  833. subcategory: trioNotificationsSubcategory,
  834. name: String(localized: "Glucose Notifications"),
  835. value: trioSettings.glucoseNotificationsOption.rawValue
  836. )
  837. addSetting(
  838. category: notificationsCategory,
  839. subcategory: trioNotificationsSubcategory,
  840. name: String(localized: "Add Glucose Source to Alarm"),
  841. value: trioSettings
  842. .addSourceInfoToGlucoseNotifications ? String(localized: "Enabled") : String(localized: "Disabled")
  843. )
  844. addSetting(
  845. category: notificationsCategory,
  846. subcategory: trioNotificationsSubcategory,
  847. name: String(localized: "Low Glucose Alarm Limit"),
  848. value: trioSettings
  849. .units == .mgdL ? String(describing: trioSettings.lowGlucose) :
  850. String(describing: trioSettings.lowGlucose.asMmolL),
  851. unit: trioSettings.units.rawValue
  852. )
  853. addSetting(
  854. category: notificationsCategory,
  855. subcategory: trioNotificationsSubcategory,
  856. name: String(localized: "High Glucose Alarm Limit"),
  857. value: trioSettings
  858. .units == .mgdL ? String(describing: trioSettings.highGlucose) :
  859. String(describing: trioSettings.highGlucose.asMmolL),
  860. unit: trioSettings.units.rawValue
  861. )
  862. // Live Activity subcategory
  863. let liveActivitySubcategory = String(localized: "Live Activity")
  864. addSetting(
  865. category: notificationsCategory,
  866. subcategory: liveActivitySubcategory,
  867. name: String(localized: "Enable Live Activity"),
  868. value: trioSettings.useLiveActivity ? String(localized: "Enabled") : String(localized: "Disabled")
  869. )
  870. addSetting(
  871. category: notificationsCategory,
  872. subcategory: liveActivitySubcategory,
  873. name: String(localized: "Lock Screen Widget Style"),
  874. value: trioSettings.lockScreenView.rawValue
  875. )
  876. }
  877. // Services
  878. if categoriesToExport.contains(.services) {
  879. let servicesCategory = String(localized: "Services", comment: "Services menu item in the Settings main view.")
  880. // Nightscout subcategory
  881. let nightscoutSubcategory = String(localized: "Nightscout")
  882. addSetting(
  883. category: servicesCategory,
  884. subcategory: nightscoutSubcategory,
  885. name: String(localized: "Allow Uploading to Nightscout"),
  886. value: trioSettings.isUploadEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
  887. )
  888. addSetting(
  889. category: servicesCategory,
  890. subcategory: nightscoutSubcategory,
  891. name: String(localized: "Upload Glucose"),
  892. value: trioSettings.uploadGlucose ? String(localized: "Enabled") : String(localized: "Disabled")
  893. )
  894. addSetting(
  895. category: servicesCategory,
  896. subcategory: nightscoutSubcategory,
  897. name: String(localized: "Allow Fetching From Nightscout"),
  898. value: trioSettings.isDownloadEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
  899. )
  900. // Apple Health subcategory
  901. let appleHealthSubcategory = String(localized: "Apple Health")
  902. addSetting(
  903. category: servicesCategory,
  904. subcategory: appleHealthSubcategory,
  905. name: String(localized: "Apple Health"),
  906. value: trioSettings.useAppleHealth ? String(localized: "Enabled") : String(localized: "Disabled")
  907. )
  908. }
  909. // Temp Target Presets
  910. if categoriesToExport.contains(.tempTargetPresets) {
  911. let category = String(localized: "Temp Target Presets")
  912. debug(.default, "🔄 EXPORT: Fetching temp target presets...")
  913. let tempTargetPresetIDs = (try? await tempTargetsStorage.fetchForTempTargetPresets()) ?? []
  914. debug(.default, "🔄 EXPORT: Found \(tempTargetPresetIDs.count) temp target preset IDs")
  915. if !tempTargetPresetIDs.isEmpty {
  916. do {
  917. let tempTargetPresets: [ExportSetting] = try await viewContext.perform {
  918. let fetchedTempTargetPresets: [TempTargetStored] = try tempTargetPresetIDs.map {
  919. guard let obj = try self.viewContext.existingObject(with: $0) as? TempTargetStored else {
  920. throw ExportError.unknown("TempTargetStored type mismatch for objectID \($0)")
  921. }
  922. return obj
  923. }
  924. var processedTempTargetPresets: [ExportSetting] = []
  925. processedTempTargetPresets.reserveCapacity(fetchedTempTargetPresets.count * 10)
  926. for preset in fetchedTempTargetPresets {
  927. let presetName = preset.name ?? "Unknown Temp Target"
  928. if let target = preset.target {
  929. let targetValue = trioSettings.units == .mgdL
  930. ? target.description
  931. : target.decimalValue.formattedAsMmolL
  932. processedTempTargetPresets.append(.init(
  933. category: category,
  934. subcategory: presetName,
  935. name: "Target",
  936. value: targetValue,
  937. unit: trioSettings.units.rawValue
  938. ))
  939. }
  940. if let duration = preset.duration {
  941. processedTempTargetPresets.append(.init(
  942. category: category,
  943. subcategory: presetName,
  944. name: "Duration",
  945. value: String(describing: duration),
  946. unit: String(localized: "minutes")
  947. ))
  948. }
  949. if let halfBasalTarget = preset.halfBasalTarget {
  950. let halfBasalValue = trioSettings.units == .mgdL
  951. ? halfBasalTarget.description
  952. : halfBasalTarget.decimalValue.formattedAsMmolL
  953. processedTempTargetPresets.append(.init(
  954. category: category,
  955. subcategory: presetName,
  956. name: "Half Basal Target",
  957. value: halfBasalValue,
  958. unit: trioSettings.units.rawValue
  959. ))
  960. }
  961. }
  962. return processedTempTargetPresets
  963. }
  964. exportSettings.append(contentsOf: tempTargetPresets)
  965. debug(.default, "✅ EXPORT: Added \(tempTargetPresets.count) temp target preset rows")
  966. } catch {
  967. // STRICT: surface the real issue
  968. return .failure(.unknown("Failed to extract Temp Targets: \(error.localizedDescription)"))
  969. }
  970. }
  971. }
  972. // Override Presets
  973. if categoriesToExport.contains(.overridePresets) {
  974. let category = String(localized: "Override Presets")
  975. debug(.default, "🔄 EXPORT: Fetching override presets...")
  976. do {
  977. let overridePresetIDs = try await overrideStorage.fetchForOverridePresets()
  978. debug(.default, "🔄 EXPORT: Found \(overridePresetIDs.count) override preset IDs")
  979. if !overridePresetIDs.isEmpty {
  980. let overridePresets: [ExportSetting] = try await viewContext.perform {
  981. let fetchedOverridePresets: [OverrideStored] = try overridePresetIDs.map {
  982. guard let obj = try self.viewContext.existingObject(with: $0) as? OverrideStored else {
  983. throw ExportError.unknown("OverrideStored type mismatch for objectID \($0)")
  984. }
  985. return obj
  986. }
  987. var processedOverridePresets: [ExportSetting] = []
  988. processedOverridePresets.reserveCapacity(fetchedOverridePresets.count * 10)
  989. for preset in fetchedOverridePresets {
  990. let presetName = preset.name ?? "Unknown Override"
  991. processedOverridePresets.append(.init(
  992. category: category,
  993. subcategory: presetName,
  994. name: String(localized: "Basal Rate Adjustment"),
  995. value: String(format: "%.0f%%", preset.percentage),
  996. unit: ""
  997. ))
  998. processedOverridePresets.append(.init(
  999. category: category,
  1000. subcategory: presetName,
  1001. name: "Duration",
  1002. value: preset.indefinite
  1003. ? String(localized: "Indefinite")
  1004. : String(describing: preset.duration ?? 0),
  1005. unit: preset.indefinite ? "" : String(localized: "minutes")
  1006. ))
  1007. if let target = preset.target, target != 0 {
  1008. processedOverridePresets.append(.init(
  1009. category: category,
  1010. subcategory: presetName,
  1011. name: "Target",
  1012. value: trioSettings.units == .mgdL
  1013. ? target.description
  1014. : target.decimalValue.formattedAsMmolL,
  1015. unit: trioSettings.units.rawValue
  1016. ))
  1017. }
  1018. if preset.advancedSettings {
  1019. processedOverridePresets.append(.init(
  1020. category: category,
  1021. subcategory: presetName,
  1022. name: "Advanced Settings",
  1023. value: String(localized: "Enabled"),
  1024. unit: ""
  1025. ))
  1026. if let smbMinutes = preset.smbMinutes {
  1027. processedOverridePresets.append(.init(
  1028. category: category,
  1029. subcategory: presetName,
  1030. name: "SMB Minutes",
  1031. value: String(describing: smbMinutes),
  1032. unit: String(localized: "minutes")
  1033. ))
  1034. }
  1035. if let uamMinutes = preset.uamMinutes {
  1036. processedOverridePresets.append(.init(
  1037. category: category,
  1038. subcategory: presetName,
  1039. name: "UAM Minutes",
  1040. value: String(describing: uamMinutes),
  1041. unit: String(localized: "minutes")
  1042. ))
  1043. }
  1044. }
  1045. if preset.smbIsOff {
  1046. processedOverridePresets.append(.init(
  1047. category: category,
  1048. subcategory: presetName,
  1049. name: "SMB",
  1050. value: String(localized: "Disabled"),
  1051. unit: ""
  1052. ))
  1053. }
  1054. if preset.smbIsScheduledOff {
  1055. processedOverridePresets.append(.init(
  1056. category: category,
  1057. subcategory: presetName,
  1058. name: "SMB Scheduled",
  1059. value: String(localized: "Disabled"),
  1060. unit: ""
  1061. ))
  1062. if let start = preset.start {
  1063. processedOverridePresets.append(.init(
  1064. category: category,
  1065. subcategory: presetName,
  1066. name: "SMB Schedule Start",
  1067. value: String(describing: start),
  1068. unit: String(localized: "hours")
  1069. ))
  1070. }
  1071. if let end = preset.end {
  1072. processedOverridePresets.append(.init(
  1073. category: category,
  1074. subcategory: presetName,
  1075. name: "SMB Schedule End",
  1076. value: String(describing: end),
  1077. unit: String(localized: "hours")
  1078. ))
  1079. }
  1080. }
  1081. // affects logic...
  1082. let affects: String? = {
  1083. if preset.isfAndCr { return String(localized: "ISF and CR") }
  1084. if preset.isf, preset.cr { return String(localized: "ISF and CR") }
  1085. if preset.isf { return String(localized: "ISF") }
  1086. if preset.cr { return String(localized: "CR") }
  1087. return nil
  1088. }()
  1089. if let affects {
  1090. processedOverridePresets.append(.init(
  1091. category: category,
  1092. subcategory: presetName,
  1093. name: "Affects",
  1094. value: affects,
  1095. unit: ""
  1096. ))
  1097. }
  1098. }
  1099. return processedOverridePresets
  1100. }
  1101. exportSettings.append(contentsOf: overridePresets)
  1102. debug(.default, "✅ EXPORT: Added \(overridePresets.count) override preset rows")
  1103. }
  1104. } catch {
  1105. return .failure(.unknown("Failed to fetch override presets: \(error.localizedDescription)"))
  1106. }
  1107. }
  1108. // Meal Presets
  1109. if categoriesToExport.contains(.mealPresets) {
  1110. let presetsCategory = String(localized: "Meal Presets")
  1111. // Meal Presets (from Core Data)
  1112. do {
  1113. debug(.default, "Fetching meal presets...")
  1114. let mealPresetData = try await viewContext.perform {
  1115. let request: NSFetchRequest<MealPresetStored> = MealPresetStored.fetchRequest()
  1116. let mealPresets = try self.viewContext.fetch(request)
  1117. return mealPresets.map { preset -> (dish: String, carbs: Decimal?, fat: Decimal?, protein: Decimal?) in
  1118. (
  1119. dish: preset.dish ?? "Unknown Meal",
  1120. carbs: preset.carbs?.decimalValue,
  1121. fat: preset.fat?.decimalValue,
  1122. protein: preset.protein?.decimalValue
  1123. )
  1124. }
  1125. }
  1126. debug(.default, "Found \(mealPresetData.count) meal presets")
  1127. if !mealPresetData.isEmpty {
  1128. for mealPreset in mealPresetData {
  1129. if let carbs = mealPreset.carbs, carbs > 0 {
  1130. addSetting(
  1131. category: presetsCategory,
  1132. subcategory: mealPreset.dish,
  1133. name: "Carbs",
  1134. value: String(describing: carbs),
  1135. unit: String(localized: "g", comment: "Units for carbs")
  1136. )
  1137. }
  1138. if let fat = mealPreset.fat, fat > 0 {
  1139. addSetting(
  1140. category: presetsCategory,
  1141. subcategory: mealPreset.dish,
  1142. name: "Fat",
  1143. value: String(describing: fat),
  1144. unit: String(localized: "g", comment: "Units for carbs")
  1145. )
  1146. }
  1147. if let protein = mealPreset.protein, protein > 0 {
  1148. addSetting(
  1149. category: presetsCategory,
  1150. subcategory: mealPreset.dish,
  1151. name: "Protein",
  1152. value: String(describing: protein),
  1153. unit: String(localized: "g", comment: "Units for carbs")
  1154. )
  1155. }
  1156. }
  1157. }
  1158. } catch {
  1159. debug(.default, "Failed to fetch meal presets: \(error)")
  1160. }
  1161. }
  1162. // Convert data to the selected format and write to file
  1163. do {
  1164. let content: String
  1165. // Helper function to escape CSV values
  1166. func csvEscape(_ value: String) -> String {
  1167. if value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r") {
  1168. return "\"\(value.replacingOccurrences(of: "\"", with: "\"\""))\""
  1169. }
  1170. return value
  1171. }
  1172. var csvContent = "\u{FEFF}Setting Category,Subcategory,Setting Name,Value,Unit\n"
  1173. for setting in exportSettings {
  1174. csvContent +=
  1175. "\(csvEscape(setting.category)),\(csvEscape(setting.subcategory)),\(csvEscape(setting.name)),\(csvEscape(setting.value)),\(csvEscape(setting.unit))\n"
  1176. }
  1177. content = csvContent
  1178. debug(
  1179. .default,
  1180. "📝 EXPORT: Writing .CSV content (\(content.count) characters) to file: \(fileURL.path)"
  1181. )
  1182. debug(.default, "📝 EXPORT: Temporary directory: \(FileManager.default.temporaryDirectory.path)")
  1183. debug(.default, "📝 EXPORT: File URL: \(fileURL)")
  1184. try content.write(to: fileURL, atomically: true, encoding: .utf8)
  1185. debug(.default, "✅ EXPORT: Content written to file successfully")
  1186. // Set file attributes for better sharing compatibility
  1187. try fileManager.setAttributes([
  1188. .posixPermissions: 0o644,
  1189. .extensionHidden: false
  1190. ], ofItemAtPath: fileURL.path)
  1191. debug(.default, "✅ EXPORT: File attributes set successfully")
  1192. // Verify file was written successfully
  1193. let fileExists = fileManager.fileExists(atPath: fileURL.path)
  1194. let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path)
  1195. let fileSize = (fileAttributes?[.size] as? NSNumber)?.intValue ?? 0
  1196. debug(.default, "📊 EXPORT: File verification - Exists: \(fileExists), Size: \(fileSize) bytes")
  1197. if !fileExists {
  1198. debug(.default, "❌ EXPORT: CRITICAL - File does not exist after writing!")
  1199. return .failure(.unknown("File was not created successfully"))
  1200. }
  1201. if fileSize == 0 {
  1202. debug(.default, "❌ EXPORT: CRITICAL - File exists but has 0 bytes!")
  1203. return .failure(.unknown("File was created but is empty"))
  1204. }
  1205. return .success(fileURL)
  1206. } catch {
  1207. debug(.default, "Failed to write settings export file: \(error)")
  1208. return .failure(.fileWriteError(error))
  1209. }
  1210. }
  1211. /// Exports settings using the currently selected categories and format
  1212. func exportSelectedSettings() async -> Result<URL, ExportError> {
  1213. await exportSettings(categories: selectedCategories)
  1214. }
  1215. /// Toggle all categories on or off
  1216. func toggleAllCategories(_ enabled: Bool) {
  1217. if enabled {
  1218. selectedCategories = Set(ExportCategory.allCases)
  1219. } else {
  1220. selectedCategories = []
  1221. }
  1222. }
  1223. /// Check if all categories are selected
  1224. var allCategoriesSelected: Bool {
  1225. selectedCategories.count == ExportCategory.allCases.count
  1226. }
  1227. }
  1228. }