| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330 |
- 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
- )
- addSetting(
- category: featuresCategory,
- subcategory: userInterfaceSubcategory,
- name: String(localized: "Require Adjustments Confirmation"),
- value: trioSettings
- .requireAdjustmentsConfirmation ? String(localized: "Enabled") : String(localized: "Disabled")
- )
- // 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
- }
- }
- }
|