| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323 |
- import CoreData
- import Foundation
- import LoopKit
- import SwiftUI
- import Swinject
- extension SettingsExport {
- final class StateModel: BaseStateModel<Provider> {
- @Injected() private var broadcaster: Broadcaster!
- @Injected() private var fileManager: FileManager!
- @Injected() private var storage: FileStorage!
- @Injected() var overrideStorage: OverrideStorage!
- @Injected() var tempTargetsStorage: TempTargetsStorage!
- // Help Sheet
- var isHelpSheetPresented: Bool = false
- var helpSheetDetent = PresentationDetent.large
- // Version information
- private var versionNumber: String = ""
- private var buildNumber: String = ""
- private var branch: String = ""
- let viewContext = CoreDataStack.shared.persistentContainer.viewContext
- override func subscribe() {
- versionNumber = Bundle.main.appDevVersion ?? "Unknown"
- buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
- branch = BuildDetails.shared.branchAndSha
- }
- // Export categories for selective export
- enum ExportCategory: String, CaseIterable, Identifiable {
- var id: String { rawValue }
- case metadata = "Metadata"
- case devices = "Devices"
- case therapy = "Therapy"
- case algorithm = "Algorithm"
- case features = "Features"
- case notifications = "Notifications"
- case services = "Services"
- case tempTargetPresets = "Temp Target Presets"
- case overridePresets = "Override Presets"
- case mealPresets = "Meal Presets"
- }
- // Published state for UI binding
- @Published var selectedCategories: Set<ExportCategory> = Set(ExportCategory.allCases)
- @Published var isExporting: Bool = false
- enum ExportError: LocalizedError {
- case documentsDirectoryNotFound
- case fileWriteError(Error)
- case unknown(String)
- var errorDescription: String? {
- switch self {
- case .documentsDirectoryNotFound:
- return String(localized: "Could not access documents directory")
- case let .fileWriteError(error):
- return String(localized: "Failed to write export file: \(error.localizedDescription)")
- case let .unknown(message):
- return String(localized: "Export failed: \(message)")
- }
- }
- }
- /// Exports selected Trio settings to a CSV file
- ///
- /// This function creates an export of the user's selected Trio configuration categories including:
- /// - Export metadata (date, app version, build) [optional]
- /// - Device settings (CGM, pump information) [optional]
- /// - Therapy profiles (basal rates, ISF, carb ratios, targets) [optional]
- /// - Algorithm settings (SMB, autosens, dynamic settings, etc.) [optional]
- /// - Features and UI preferences [optional]
- /// - Notification settings [optional]
- /// - Service configurations [optional]
- /// - Preset data [optional]
- ///
- /// - Parameter categories: Set of categories to include in export. If nil, exports all categories.
- /// - Parameter format: Export format to use. If nil, uses currently selected format.
- /// - Returns: A Result containing either the file URL on success or an ExportError on failure
- func exportSettings(
- categories: Set<ExportCategory>? = nil
- ) async -> Result<URL, ExportError> {
- debug(.default, "🔄 EXPORT: Starting settings export...")
- await MainActor.run { isExporting = true }
- defer { Task { @MainActor in isExporting = false } }
- let categoriesToExport = categories ?? selectedCategories
- debug(
- .default,
- "🔄 EXPORT: Exporting categories: \(categoriesToExport.map(\.rawValue).joined(separator: ", ")) in .CSV format"
- )
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyyMMdd_HHmmss"
- let timestamp = formatter.string(from: Date())
- let fileName = "TrioSettings_\(timestamp).csv"
- // Use the Documents directory for better sharing compatibility
- guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
- return .failure(.documentsDirectoryNotFound)
- }
- let fileURL = documentsDirectory.appendingPathComponent(fileName)
- debug(.default, "Export file path: \(fileURL.path)")
- var exportSettings: [ExportSetting] = []
- let trioSettings = settingsManager.settings
- let preferences = settingsManager.preferences
- debug(.default, "🔄 EXPORT: Settings managers initialized")
- // Helper function to add a setting
- func addSetting(category: String, subcategory: String = "", name: String, value: String, unit: String = "") {
- exportSettings.append(ExportSetting(
- category: category,
- subcategory: subcategory,
- name: name,
- value: value,
- unit: unit
- ))
- }
- // Export metadata - always include basic export info
- if categoriesToExport.contains(.metadata) {
- let exportCategory = String(localized: "Metadata")
- addSetting(
- category: exportCategory,
- name: String(localized: "Export Date"),
- value: DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium)
- )
- addSetting(category: exportCategory, name: String(localized: "App Version"), value: versionNumber)
- addSetting(category: exportCategory, name: String(localized: "Build Number"), value: buildNumber)
- addSetting(category: exportCategory, name: String(localized: "Branch"), value: branch)
- }
- // Devices
- if categoriesToExport.contains(.devices) {
- let devicesCategory = String(localized: "Devices", comment: "Devices menu item in the Settings main view.")
- addSetting(category: devicesCategory, name: String(localized: "CGM"), value: trioSettings.cgm.rawValue)
- addSetting(
- category: devicesCategory,
- name: String(localized: "Smooth Glucose Value"),
- value: trioSettings.smoothGlucose ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Pump Information
- if let pumpManager = provider.deviceManager.pumpManager {
- addSetting(category: devicesCategory, name: String(localized: "Pump Type"), value: pumpManager.localizedTitle)
- // Get insulin type from pump manager if available, otherwise from preferences
- let insulinTypeValue: String
- if let pumpManager = provider.deviceManager.pumpManager,
- let insulinType = pumpManager.status.insulinType
- {
- insulinTypeValue = insulinType.title
- } else {
- insulinTypeValue = preferences.curve.rawValue
- // technically, this gets set only when a pump is onboared
- // leaving this here as a backup, because you theoretically could
- // have removed your PM instance, but are just within pumps and
- // insulin type stays the same.
- // however, this theoretically could be a stale type.
- }
- addSetting(
- category: devicesCategory,
- name: String(localized: "Insulin Type"),
- value: insulinTypeValue
- )
- } else {
- addSetting(
- category: devicesCategory,
- name: String(localized: "Pump Type"),
- value: String(localized: "Not Connected")
- )
- }
- }
- // Therapy Settings
- if categoriesToExport.contains(.therapy) {
- let therapyCategory = String(localized: "Therapy", comment: "Therapy menu item in the Settings main view.")
- // Units and Limits subcategory
- let unitsLimitsSubcategory = String(localized: "Units and Limits")
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Glucose Units"),
- value: trioSettings.units.rawValue
- )
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Maximum Insulin on Board (IOB)"),
- value: String(describing: preferences.maxIOB),
- unit: "U"
- )
- // Add missing pump settings from PumpSettings
- let pumpSettings = settingsManager.pumpSettings
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Maximum Bolus"),
- value: String(describing: pumpSettings.maxBolus),
- unit: "U"
- )
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Maximum Basal Rate"),
- value: String(describing: pumpSettings.maxBasal),
- unit: String(localized: "U/hr", comment: "Insulin unit per hour abbreviation")
- )
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Maximum Carbs on Board (COB)"),
- value: String(describing: preferences.maxCOB),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: therapyCategory,
- subcategory: unitsLimitsSubcategory,
- name: String(localized: "Minimum Safety Threshold"),
- value: trioSettings
- .units == .mgdL ? String(describing: preferences.threshold_setting) :
- String(describing: preferences.threshold_setting.asMmolL),
- unit: trioSettings.units.rawValue
- )
- // Get therapy profiles from storage
- let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) ?? []
- let isfProfileContainer = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
- let crProfileContainer = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
- let targetProfileContainer = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
- // Glucose Targets subcategory
- let glucoseTargetsSubcategory = String(localized: "Glucose Targets")
- if let targetContainer = targetProfileContainer {
- for entry in targetContainer.targets {
- // Export single target value since high==low in Trio
- let targetValue = trioSettings.units == .mgdL ? entry.low : entry.low.asMmolL
- addSetting(
- category: therapyCategory,
- subcategory: glucoseTargetsSubcategory,
- name: String(localized: "Target (\(entry.start.formattedHourMinuteFromTimeString()))"),
- value: String(describing: targetValue),
- unit: trioSettings.units.rawValue
- )
- }
- }
- // Basal Rates subcategory
- let basalRatesSubcategory = String(localized: "Basal Rates")
- for entry in basalProfile {
- addSetting(
- category: therapyCategory,
- subcategory: basalRatesSubcategory,
- name: String(localized: "Basal Rate (\(entry.start.formattedHourMinuteFromTimeString()))"),
- value: String(describing: entry.rate),
- unit: String(localized: "U/hr", comment: "Insulin unit per hour abbreviation")
- )
- }
- // Carb Ratios subcategory
- let carbRatiosSubcategory = String(localized: "Carb Ratios")
- if let crContainer = crProfileContainer {
- for entry in crContainer.schedule {
- addSetting(
- category: therapyCategory,
- subcategory: carbRatiosSubcategory,
- name: String(localized: "Carb Ratio (\(entry.start.formattedHourMinuteFromTimeString()))"),
- value: String(describing: entry.ratio),
- unit: String(localized: "g/U")
- )
- }
- }
- // Insulin Sensitivities subcategory
- let insulinSensitivitiesSubcategory = String(localized: "Insulin Sensitivities")
- if let isfContainer = isfProfileContainer {
- for entry in isfContainer.sensitivities {
- let isfValue = trioSettings.units == .mgdL ? entry.sensitivity : entry.sensitivity.asMmolL
- addSetting(
- category: therapyCategory,
- subcategory: insulinSensitivitiesSubcategory,
- name: String(localized: "ISF (\(entry.start.formattedHourMinuteFromTimeString()))"),
- value: String(describing: isfValue),
- unit: trioSettings.units.rawValue
- )
- }
- }
- }
- // Algorithm Settings
- if categoriesToExport.contains(.algorithm) {
- let algorithmCategory = String(localized: "Algorithm", comment: "Algorithm menu item in the Settings main view.")
- let pumpSettings = settingsManager.pumpSettings
- // Autosens Settings
- let autosensSubcategory = String(localized: "Autosens")
- addSetting(
- category: algorithmCategory,
- subcategory: autosensSubcategory,
- name: String(localized: "Autosens Max"),
- value: String(format: "%.0f", (preferences.autosensMax as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: autosensSubcategory,
- name: String(localized: "Autosens Min"),
- value: String(format: "%.0f", (preferences.autosensMin as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: autosensSubcategory,
- name: String(localized: "Rewind Resets Autosens"),
- value: preferences.rewindResetsAutosens ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // SMB Settings
- let smbSubcategory = String(localized: "SMB")
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable SMB Always"),
- value: preferences.enableSMBAlways ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable SMB With COB"),
- value: preferences.enableSMBWithCOB ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable SMB With Temptarget"),
- value: preferences.enableSMBWithTemptarget ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable SMB After Carbs"),
- value: preferences.enableSMBAfterCarbs ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable SMB With High Glucose"),
- value: preferences.enableSMB_high_bg ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- if preferences.enableSMB_high_bg {
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "High Glucose Target"),
- value: trioSettings
- .units == .mgdL ? String(describing: preferences.enableSMB_high_bg_target) :
- String(describing: preferences.enableSMB_high_bg_target.asMmolL),
- unit: trioSettings.units.rawValue
- )
- }
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Allow SMB With High Temptarget"),
- value: preferences.allowSMBWithHighTemptarget ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Enable UAM"),
- value: preferences.enableUAM ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Max SMB Basal Minutes"),
- value: String(describing: preferences.maxSMBBasalMinutes),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Max UAM Basal Minutes"),
- value: String(describing: preferences.maxUAMSMBBasalMinutes),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: smbSubcategory,
- name: String(localized: "Max Allowed Glucose Rise for SMB"),
- value: String(format: "%.0f", (preferences.maxDeltaBGthreshold as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- // Dynamic Settings
- let dynamicSubcategory = String(localized: "Dynamic Settings")
- // Proper Dynamic ISF handling using the current enum logic
- let dynamicISFValue: String
- if !preferences.useNewFormula {
- dynamicISFValue = String(localized: "Disabled")
- } else if preferences.sigmoid {
- dynamicISFValue = String(localized: "Sigmoid")
- } else {
- dynamicISFValue = String(localized: "Logarithmic")
- }
- addSetting(
- category: algorithmCategory,
- subcategory: dynamicSubcategory,
- name: String(localized: "Dynamic ISF"),
- value: dynamicISFValue
- )
- // Show adjustment factors as percentages with proper labels
- if preferences.useNewFormula {
- if !preferences.sigmoid {
- addSetting(
- category: algorithmCategory,
- subcategory: dynamicSubcategory,
- name: String(localized: "Adjustment Factor (AF)"),
- value: String(format: "%.0f", (preferences.adjustmentFactor as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- } else {
- addSetting(
- category: algorithmCategory,
- subcategory: dynamicSubcategory,
- name: String(localized: "Sigmoid Adjustment Factor"),
- value: String(
- format: "%.0f",
- (preferences.adjustmentFactorSigmoid as NSDecimalNumber).doubleValue * 100
- ),
- unit: "%"
- )
- }
- }
- // Weighted Average of TDD is shown for both logarithmic and sigmoid when Dynamic ISF is enabled
- addSetting(
- category: algorithmCategory,
- subcategory: dynamicSubcategory,
- name: String(localized: "Weighted Average of TDD"),
- value: String(format: "%.0f", (preferences.weightPercentage as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: dynamicSubcategory,
- name: String(localized: "Adjust Basal"),
- value: preferences.tddAdjBasal ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Target Behavior
- let targetBehaviorSubcategory = String(localized: "Target Behavior")
- addSetting(
- category: algorithmCategory,
- subcategory: targetBehaviorSubcategory,
- name: String(localized: "High Temptarget Raises Sensitivity"),
- value: preferences
- .highTemptargetRaisesSensitivity ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: targetBehaviorSubcategory,
- name: String(localized: "Low Temptarget Lowers Sensitivity"),
- value: preferences
- .lowTemptargetLowersSensitivity ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: targetBehaviorSubcategory,
- name: String(localized: "Sensitivity Raises Target"),
- value: preferences.sensitivityRaisesTarget ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: targetBehaviorSubcategory,
- name: String(localized: "Resistance Lowers Target"),
- value: preferences.resistanceLowersTarget ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: targetBehaviorSubcategory,
- name: String(localized: "Half Basal Exercise Target"),
- value: trioSettings
- .units == .mgdL ? String(describing: preferences.halfBasalExerciseTarget) :
- String(describing: preferences.halfBasalExerciseTarget.asMmolL),
- unit: trioSettings.units.rawValue
- )
- // Additional Algorithm Settings
- let additionalsSubcategory = String(localized: "Additionals")
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Max Daily Safety Multiplier"),
- value: String(format: "%.0f", (preferences.maxDailySafetyMultiplier as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Current Basal Safety Multiplier"),
- value: String(
- format: "%.0f",
- (preferences.currentBasalSafetyMultiplier as NSDecimalNumber).doubleValue * 100
- ),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Use Custom Peak Time"),
- value: preferences.useCustomPeakTime ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Duration of Insulin Action"),
- value: String(describing: pumpSettings.insulinActionCurve),
- unit: String(localized: "hours")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Insulin Peak Time"),
- value: String(describing: preferences.insulinPeakTime),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Skip Neutral Temps"),
- value: preferences.skipNeutralTemps ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Unsuspend If No Temp"),
- value: preferences.unsuspendIfNoTemp ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Suspend Zeros IOB"),
- value: preferences.suspendZerosIOB ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // SMB settings that belong in Additionals (correct order based on UI)
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "SMB Delivery Ratio"),
- value: String(format: "%.0f", (preferences.smbDeliveryRatio as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "SMB Interval"),
- value: String(describing: preferences.smbInterval),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Min 5m Carb Impact"),
- value: trioSettings
- .units == .mgdL ? String(describing: preferences.min5mCarbimpact) :
- String(describing: preferences.min5mCarbimpact.asMmolL),
- unit: trioSettings.units.rawValue
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Remaining Carbs Percentage"),
- value: String(format: "%.0f", (preferences.remainingCarbsFraction as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Remaining Carbs Cap"),
- value: String(describing: preferences.remainingCarbsCap),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: algorithmCategory,
- subcategory: additionalsSubcategory,
- name: String(localized: "Noisy CGM Target Increase"),
- value: String(format: "%.0f", (preferences.noisyCGMTargetMultiplier as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- }
- // Features
- if categoriesToExport.contains(.features) {
- let featuresCategory = String(localized: "Features", comment: "Features menu item in the Settings main view.")
- // Trio Features subcategory - Bolus Calculator
- let bolusCalculatorSubcategory = String(localized: "Bolus Calculator")
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Display Meal Presets"),
- value: trioSettings.displayPresets ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Recommended Bolus Percentage"),
- value: String(format: "%.0f", (trioSettings.overrideFactor as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Enable Reduced Bolus Option"),
- value: trioSettings.fattyMeals ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- if trioSettings.fattyMeals {
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Reduced Bolus Percentage"),
- value: String(format: "%.0f", (trioSettings.fattyMealFactor as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- }
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Enable Super Bolus Option"),
- value: trioSettings.sweetMeals ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- if trioSettings.sweetMeals {
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Super Bolus Percentage"),
- value: String(format: "%.0f", (trioSettings.sweetMealFactor as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- }
- addSetting(
- category: featuresCategory,
- subcategory: bolusCalculatorSubcategory,
- name: String(localized: "Very Low Glucose Warning"),
- value: trioSettings.confirmBolus ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Trio Features subcategory - Meal Settings
- let mealSettingsSubcategory = String(localized: "Meal Settings")
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Max Carbs"),
- value: String(describing: trioSettings.maxCarbs),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Max Fat"),
- value: String(describing: trioSettings.maxFat),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Max Protein"),
- value: String(describing: trioSettings.maxProtein),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Max Meal Absorption Time"),
- value: String(describing: preferences.maxMealAbsorptionTime),
- unit: String(localized: "hours")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Enable Fat and Protein Entries"),
- value: trioSettings.useFPUconversion ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Fat and Protein Delay"),
- value: String(describing: trioSettings.delay),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Spread Interval"),
- value: String(describing: trioSettings.minuteInterval),
- unit: String(localized: "minutes")
- )
- addSetting(
- category: featuresCategory,
- subcategory: mealSettingsSubcategory,
- name: String(localized: "Fat and Protein Percentage"),
- value: String(format: "%.0f", (trioSettings.individualAdjustmentFactor as NSDecimalNumber).doubleValue * 100),
- unit: "%"
- )
- // Trio Features subcategory - Shortcuts
- let shortcutsSubcategory = String(localized: "Shortcuts")
- addSetting(
- category: featuresCategory,
- subcategory: shortcutsSubcategory,
- name: String(localized: "Allow Bolusing with Shortcuts"),
- value: trioSettings
- .bolusShortcut != .notAllowed ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Trio Features subcategory - Remote Control
- let remoteControlSubcategory = String(localized: "Remote Control")
- let isRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
- addSetting(
- category: featuresCategory,
- subcategory: remoteControlSubcategory,
- name: String(localized: "Enable Remote Control"),
- value: isRemoteControlEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Trio Personalization subcategory - User Interface
- let userInterfaceSubcategory = String(localized: "User Interface")
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Show X-Axis Grid Lines"),
- value: trioSettings.xGridLines ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Show Y-Axis Grid Lines"),
- value: trioSettings.yGridLines ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Show Low and High Thresholds"),
- value: trioSettings.rulerMarks ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Low Threshold"),
- value: trioSettings
- .units == .mgdL ? String(describing: trioSettings.low) : String(describing: trioSettings.low.asMmolL),
- unit: trioSettings.units.rawValue
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "High Threshold"),
- value: trioSettings
- .units == .mgdL ? String(describing: trioSettings.high) : String(describing: trioSettings.high.asMmolL),
- unit: trioSettings.units.rawValue
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "eA1c/GMI Display Unit"),
- value: trioSettings.eA1cDisplayUnit.rawValue
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Show Carbs Required Badge"),
- value: trioSettings.showCarbsRequiredBadge ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Carbs Required Threshold"),
- value: String(describing: trioSettings.carbsRequiredThreshold),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Forecast Display Type"),
- value: trioSettings.forecastDisplayType.rawValue
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Glucose Color Scheme"),
- value: trioSettings.glucoseColorScheme.rawValue
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Time in Range Type"),
- value: trioSettings.timeInRangeType.rawValue
- )
- // Appearance setting from UserDefaults
- let colorSchemePreference = UserDefaults.standard.string(forKey: "colorSchemePreference") ?? "systemDefault"
- let appearanceValue: String
- switch colorSchemePreference {
- case "systemDefault":
- appearanceValue = String(localized: "System Default")
- case "light":
- appearanceValue = String(localized: "Light")
- case "dark":
- appearanceValue = String(localized: "Dark")
- default:
- appearanceValue = String(localized: "System Default")
- }
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Appearance"),
- value: appearanceValue
- )
- }
- // Notifications
- if categoriesToExport.contains(.notifications) {
- let notificationsCategory = String(
- localized: "Notifications",
- comment: "Notifications menu item in the Settings main view."
- )
- // Trio Notifications subcategory
- let trioNotificationsSubcategory = String(localized: "Trio Notifications")
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Always Notify Pump"),
- value: trioSettings.notificationsPump ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Always Notify CGM"),
- value: trioSettings.notificationsCgm ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Always Notify Carb"),
- value: trioSettings.notificationsCarb ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Always Notify Algorithm"),
- value: trioSettings.notificationsAlgorithm ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Show Glucose App Badge"),
- value: trioSettings.glucoseBadge ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Glucose Notifications"),
- value: trioSettings.glucoseNotificationsOption.rawValue
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Add Glucose Source to Alarm"),
- value: trioSettings
- .addSourceInfoToGlucoseNotifications ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "Low Glucose Alarm Limit"),
- value: trioSettings
- .units == .mgdL ? String(describing: trioSettings.lowGlucose) :
- String(describing: trioSettings.lowGlucose.asMmolL),
- unit: trioSettings.units.rawValue
- )
- addSetting(
- category: notificationsCategory,
- subcategory: trioNotificationsSubcategory,
- name: String(localized: "High Glucose Alarm Limit"),
- value: trioSettings
- .units == .mgdL ? String(describing: trioSettings.highGlucose) :
- String(describing: trioSettings.highGlucose.asMmolL),
- unit: trioSettings.units.rawValue
- )
- // Live Activity subcategory
- let liveActivitySubcategory = String(localized: "Live Activity")
- addSetting(
- category: notificationsCategory,
- subcategory: liveActivitySubcategory,
- name: String(localized: "Enable Live Activity"),
- value: trioSettings.useLiveActivity ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: notificationsCategory,
- subcategory: liveActivitySubcategory,
- name: String(localized: "Lock Screen Widget Style"),
- value: trioSettings.lockScreenView.rawValue
- )
- }
- // Services
- if categoriesToExport.contains(.services) {
- let servicesCategory = String(localized: "Services", comment: "Services menu item in the Settings main view.")
- // Nightscout subcategory
- let nightscoutSubcategory = String(localized: "Nightscout")
- addSetting(
- category: servicesCategory,
- subcategory: nightscoutSubcategory,
- name: String(localized: "Allow Uploading to Nightscout"),
- value: trioSettings.isUploadEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: servicesCategory,
- subcategory: nightscoutSubcategory,
- name: String(localized: "Upload Glucose"),
- value: trioSettings.uploadGlucose ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- addSetting(
- category: servicesCategory,
- subcategory: nightscoutSubcategory,
- name: String(localized: "Allow Fetching From Nightscout"),
- value: trioSettings.isDownloadEnabled ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // Apple Health subcategory
- let appleHealthSubcategory = String(localized: "Apple Health")
- addSetting(
- category: servicesCategory,
- subcategory: appleHealthSubcategory,
- name: String(localized: "Apple Health"),
- value: trioSettings.useAppleHealth ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- }
- // Temp Target Presets
- if categoriesToExport.contains(.tempTargetPresets) {
- let category = String(localized: "Temp Target Presets")
- debug(.default, "🔄 EXPORT: Fetching temp target presets...")
- let tempTargetPresetIDs = (try? await tempTargetsStorage.fetchForTempTargetPresets()) ?? []
- debug(.default, "🔄 EXPORT: Found \(tempTargetPresetIDs.count) temp target preset IDs")
- if !tempTargetPresetIDs.isEmpty {
- do {
- let tempTargetPresets: [ExportSetting] = try await viewContext.perform {
- let fetchedTempTargetPresets: [TempTargetStored] = try tempTargetPresetIDs.map {
- guard let obj = try self.viewContext.existingObject(with: $0) as? TempTargetStored else {
- throw ExportError.unknown("TempTargetStored type mismatch for objectID \($0)")
- }
- return obj
- }
- var processedTempTargetPresets: [ExportSetting] = []
- processedTempTargetPresets.reserveCapacity(fetchedTempTargetPresets.count * 10)
- for preset in fetchedTempTargetPresets {
- let presetName = preset.name ?? "Unknown Temp Target"
- if let target = preset.target {
- let targetValue = trioSettings.units == .mgdL
- ? target.description
- : target.decimalValue.formattedAsMmolL
- processedTempTargetPresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Target",
- value: targetValue,
- unit: trioSettings.units.rawValue
- ))
- }
- if let duration = preset.duration {
- processedTempTargetPresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Duration",
- value: String(describing: duration),
- unit: String(localized: "minutes")
- ))
- }
- if let halfBasalTarget = preset.halfBasalTarget {
- let halfBasalValue = trioSettings.units == .mgdL
- ? halfBasalTarget.description
- : halfBasalTarget.decimalValue.formattedAsMmolL
- processedTempTargetPresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Half Basal Target",
- value: halfBasalValue,
- unit: trioSettings.units.rawValue
- ))
- }
- }
- return processedTempTargetPresets
- }
- exportSettings.append(contentsOf: tempTargetPresets)
- debug(.default, "✅ EXPORT: Added \(tempTargetPresets.count) temp target preset rows")
- } catch {
- // STRICT: surface the real issue
- return .failure(.unknown("Failed to extract Temp Targets: \(error.localizedDescription)"))
- }
- }
- }
- // Override Presets
- if categoriesToExport.contains(.overridePresets) {
- let category = String(localized: "Override Presets")
- debug(.default, "🔄 EXPORT: Fetching override presets...")
- do {
- let overridePresetIDs = try await overrideStorage.fetchForOverridePresets()
- debug(.default, "🔄 EXPORT: Found \(overridePresetIDs.count) override preset IDs")
- if !overridePresetIDs.isEmpty {
- let overridePresets: [ExportSetting] = try await viewContext.perform {
- let fetchedOverridePresets: [OverrideStored] = try overridePresetIDs.map {
- guard let obj = try self.viewContext.existingObject(with: $0) as? OverrideStored else {
- throw ExportError.unknown("OverrideStored type mismatch for objectID \($0)")
- }
- return obj
- }
- var processedOverridePresets: [ExportSetting] = []
- processedOverridePresets.reserveCapacity(fetchedOverridePresets.count * 10)
- for preset in fetchedOverridePresets {
- let presetName = preset.name ?? "Unknown Override"
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: String(localized: "Basal Rate Adjustment"),
- value: String(format: "%.0f%%", preset.percentage),
- unit: ""
- ))
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Duration",
- value: preset.indefinite
- ? String(localized: "Indefinite")
- : String(describing: preset.duration ?? 0),
- unit: preset.indefinite ? "" : String(localized: "minutes")
- ))
- if let target = preset.target, target != 0 {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Target",
- value: trioSettings.units == .mgdL
- ? target.description
- : target.decimalValue.formattedAsMmolL,
- unit: trioSettings.units.rawValue
- ))
- }
- if preset.advancedSettings {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Advanced Settings",
- value: String(localized: "Enabled"),
- unit: ""
- ))
- if let smbMinutes = preset.smbMinutes {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "SMB Minutes",
- value: String(describing: smbMinutes),
- unit: String(localized: "minutes")
- ))
- }
- if let uamMinutes = preset.uamMinutes {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "UAM Minutes",
- value: String(describing: uamMinutes),
- unit: String(localized: "minutes")
- ))
- }
- }
- if preset.smbIsOff {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "SMB",
- value: String(localized: "Disabled"),
- unit: ""
- ))
- }
- if preset.smbIsScheduledOff {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "SMB Scheduled",
- value: String(localized: "Disabled"),
- unit: ""
- ))
- if let start = preset.start {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "SMB Schedule Start",
- value: String(describing: start),
- unit: String(localized: "hours")
- ))
- }
- if let end = preset.end {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "SMB Schedule End",
- value: String(describing: end),
- unit: String(localized: "hours")
- ))
- }
- }
- // affects logic...
- let affects: String? = {
- if preset.isfAndCr { return String(localized: "ISF and CR") }
- if preset.isf, preset.cr { return String(localized: "ISF and CR") }
- if preset.isf { return String(localized: "ISF") }
- if preset.cr { return String(localized: "CR") }
- return nil
- }()
- if let affects {
- processedOverridePresets.append(.init(
- category: category,
- subcategory: presetName,
- name: "Affects",
- value: affects,
- unit: ""
- ))
- }
- }
- return processedOverridePresets
- }
- exportSettings.append(contentsOf: overridePresets)
- debug(.default, "✅ EXPORT: Added \(overridePresets.count) override preset rows")
- }
- } catch {
- return .failure(.unknown("Failed to fetch override presets: \(error.localizedDescription)"))
- }
- }
- // Meal Presets
- if categoriesToExport.contains(.mealPresets) {
- let presetsCategory = String(localized: "Meal Presets")
- // Meal Presets (from Core Data)
- do {
- debug(.default, "Fetching meal presets...")
- let mealPresetData = try await viewContext.perform {
- let request: NSFetchRequest<MealPresetStored> = MealPresetStored.fetchRequest()
- let mealPresets = try self.viewContext.fetch(request)
- return mealPresets.map { preset -> (dish: String, carbs: Decimal?, fat: Decimal?, protein: Decimal?) in
- (
- dish: preset.dish ?? "Unknown Meal",
- carbs: preset.carbs?.decimalValue,
- fat: preset.fat?.decimalValue,
- protein: preset.protein?.decimalValue
- )
- }
- }
- debug(.default, "Found \(mealPresetData.count) meal presets")
- if !mealPresetData.isEmpty {
- for mealPreset in mealPresetData {
- if let carbs = mealPreset.carbs, carbs > 0 {
- addSetting(
- category: presetsCategory,
- subcategory: mealPreset.dish,
- name: "Carbs",
- value: String(describing: carbs),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- }
- if let fat = mealPreset.fat, fat > 0 {
- addSetting(
- category: presetsCategory,
- subcategory: mealPreset.dish,
- name: "Fat",
- value: String(describing: fat),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- }
- if let protein = mealPreset.protein, protein > 0 {
- addSetting(
- category: presetsCategory,
- subcategory: mealPreset.dish,
- name: "Protein",
- value: String(describing: protein),
- unit: String(localized: "g", comment: "Units for carbs")
- )
- }
- }
- }
- } catch {
- debug(.default, "Failed to fetch meal presets: \(error)")
- }
- }
- // Convert data to the selected format and write to file
- do {
- let content: String
- // Helper function to escape CSV values
- func csvEscape(_ value: String) -> String {
- if value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r") {
- return "\"\(value.replacingOccurrences(of: "\"", with: "\"\""))\""
- }
- return value
- }
- var csvContent = "\u{FEFF}Setting Category,Subcategory,Setting Name,Value,Unit\n"
- for setting in exportSettings {
- csvContent +=
- "\(csvEscape(setting.category)),\(csvEscape(setting.subcategory)),\(csvEscape(setting.name)),\(csvEscape(setting.value)),\(csvEscape(setting.unit))\n"
- }
- content = csvContent
- debug(
- .default,
- "📝 EXPORT: Writing .CSV content (\(content.count) characters) to file: \(fileURL.path)"
- )
- debug(.default, "📝 EXPORT: Temporary directory: \(FileManager.default.temporaryDirectory.path)")
- debug(.default, "📝 EXPORT: File URL: \(fileURL)")
- try content.write(to: fileURL, atomically: true, encoding: .utf8)
- debug(.default, "✅ EXPORT: Content written to file successfully")
- // Set file attributes for better sharing compatibility
- try fileManager.setAttributes([
- .posixPermissions: 0o644,
- .extensionHidden: false
- ], ofItemAtPath: fileURL.path)
- debug(.default, "✅ EXPORT: File attributes set successfully")
- // Verify file was written successfully
- let fileExists = fileManager.fileExists(atPath: fileURL.path)
- let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path)
- let fileSize = (fileAttributes?[.size] as? NSNumber)?.intValue ?? 0
- debug(.default, "📊 EXPORT: File verification - Exists: \(fileExists), Size: \(fileSize) bytes")
- if !fileExists {
- debug(.default, "❌ EXPORT: CRITICAL - File does not exist after writing!")
- return .failure(.unknown("File was not created successfully"))
- }
- if fileSize == 0 {
- debug(.default, "❌ EXPORT: CRITICAL - File exists but has 0 bytes!")
- return .failure(.unknown("File was created but is empty"))
- }
- return .success(fileURL)
- } catch {
- debug(.default, "Failed to write settings export file: \(error)")
- return .failure(.fileWriteError(error))
- }
- }
- /// Exports settings using the currently selected categories and format
- func exportSelectedSettings() async -> Result<URL, ExportError> {
- await exportSettings(categories: selectedCategories)
- }
- /// Toggle all categories on or off
- func toggleAllCategories(_ enabled: Bool) {
- if enabled {
- selectedCategories = Set(ExportCategory.allCases)
- } else {
- selectedCategories = []
- }
- }
- /// Check if all categories are selected
- var allCategoriesSelected: Bool {
- selectedCategories.count == ExportCategory.allCases.count
- }
- }
- }
|