SettingItems.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import Foundation
  2. import LoopKitUI
  3. import SwiftUI
  4. struct SettingItem: Identifiable {
  5. let id = UUID()
  6. let title: String
  7. let view: Screen
  8. let searchContents: [String]?
  9. let path: [String]?
  10. /// Maps a `searchContents` string to the exact label used in `SettingInputSection`
  11. /// when the two differ (e.g. `"Max IOB"` → `"Maximum Insulin on Board (IOB)"`).
  12. /// Entries whose searchContents string already matches the label don't need an entry here.
  13. let scrollTargetLabels: [String: String]?
  14. init(
  15. title: String,
  16. view: Screen,
  17. searchContents: [String]? = nil,
  18. scrollTargetLabels: [String: String]? = nil,
  19. path: [String]? = nil
  20. ) {
  21. self.title = title
  22. self.view = view
  23. self.searchContents = searchContents
  24. self.scrollTargetLabels = scrollTargetLabels
  25. self.path = path
  26. }
  27. }
  28. struct FilteredSettingItem: Identifiable {
  29. let id = UUID()
  30. let settingItem: SettingItem
  31. let matchedContent: String
  32. /// The label string used as the scroll/highlight target in the destination view.
  33. /// Falls back to `matchedContent` when no explicit mapping exists.
  34. var scrollLabel: String {
  35. settingItem.scrollTargetLabels?[matchedContent] ?? matchedContent
  36. }
  37. }
  38. enum SettingItems {
  39. static let trioConfig = [
  40. SettingItem(title: String(localized: "Devices", comment: "Devices menu item in the Settings main view."), view: .devices),
  41. SettingItem(
  42. title: String(localized: "Therapy", comment: "Therapy menu item in the Settings main view."),
  43. view: .therapySettings
  44. ),
  45. SettingItem(
  46. title: String(localized: "Algorithm", comment: "Algorithm menu item in the Settings main view."),
  47. view: .algorithmSettings
  48. ),
  49. SettingItem(
  50. title: String(localized: "Features", comment: "Features menu item in the Settings main view."),
  51. view: .featureSettings
  52. ),
  53. SettingItem(
  54. title: String(localized: "Notifications", comment: "Notifications menu item in the Settings main view."),
  55. view: .notificationSettings
  56. ),
  57. SettingItem(
  58. title: String(localized: "Services", comment: "Services menu item in the Settings main view."),
  59. view: .serviceSettings
  60. )
  61. ]
  62. static let devicesItems = [
  63. SettingItem(title: "Insulin Pump", view: .pumpConfig, path: ["Devices"]),
  64. SettingItem(
  65. title: "CGM",
  66. view: .cgm,
  67. searchContents: ["Smooth Glucose Value"],
  68. path: ["Devices", "Continuous Glucose Monitor"]
  69. ),
  70. SettingItem(title: "Smart Watch", view: .watch, path: ["Devices"]),
  71. SettingItem(
  72. title: "Apple Watch",
  73. view: .watch,
  74. searchContents: ["Display on Watch", "Show Protein and Fat", "Confirm Bolus Faster"],
  75. path: ["Devices", "Smart Watch", "Apple Watch"]
  76. ),
  77. SettingItem(
  78. title: "Contact Image",
  79. view: .watch,
  80. searchContents: ["Display on Watch", "Watch Complication"],
  81. path: ["Devices", "Smart Watch", "Apple Watch", "Contact Image"]
  82. )
  83. ]
  84. static let therapyItems = [
  85. SettingItem(
  86. title: "Units and Limits",
  87. view: .unitsAndLimits,
  88. searchContents: [
  89. "Glucose Units",
  90. "Max Basal",
  91. "Max Bolus",
  92. "Max IOB",
  93. "Max COB",
  94. "Minimum Safety Threshold",
  95. "Delivery Limits"
  96. ],
  97. scrollTargetLabels: [
  98. "Max IOB": "Maximum Insulin on Board (IOB)",
  99. "Max Bolus": "Maximum Bolus",
  100. "Max Basal": "Maximum Basal Rate",
  101. "Max COB": "Maximum Carbs on Board (COB)"
  102. ],
  103. path: ["Therapy Settings", "Units and Limits"]
  104. ),
  105. SettingItem(title: "Basal Rates", view: .basalProfileEditor, path: ["Therapy Settings"]),
  106. SettingItem(title: "Insulin Sensitivities", view: .isfEditor, path: ["Therapy Settings"]),
  107. SettingItem(title: "ISF", view: .isfEditor, path: ["Therapy Settings"]),
  108. SettingItem(title: "Carb Ratios", view: .crEditor, path: ["Therapy Settings"]),
  109. SettingItem(title: "CR", view: .crEditor, path: ["Therapy Settings"]),
  110. SettingItem(title: "Glucose Targets", view: .targetsEditor, path: ["Therapy Settings"])
  111. ]
  112. static let algorithmItems = [
  113. SettingItem(
  114. title: "Autosens",
  115. view: .autosensSettings,
  116. searchContents: ["Autosens Max", "Autosens Min", "Rewind Resets Autosens"],
  117. path: ["Algorithm", "Autosens"]
  118. ),
  119. SettingItem(
  120. title: "Super Micro Bolus (SMB)",
  121. view: .smbSettings,
  122. searchContents: [
  123. "Enable SMB Always",
  124. "Enable SMB With COB",
  125. "Enable SMB With Temporary Target",
  126. "Enable SMB After Carbs",
  127. "Enable SMB With High Glucose",
  128. "High Glucose Target",
  129. "Allow SMB With High Temporary Target",
  130. "Enable UAM",
  131. "Max SMB Basal Minutes",
  132. "Max UAM SMB Basal Minutes",
  133. "Max Allowed Glucose Rise for SMB"
  134. ],
  135. scrollTargetLabels: [
  136. "Enable SMB With Temporary Target": "Enable SMB With Temptarget",
  137. "Allow SMB With High Temporary Target": "Allow SMB With High Temptarget",
  138. "Max UAM SMB Basal Minutes": "Max UAM Basal Minutes",
  139. "High Glucose Target": "Enable SMB With High Glucose"
  140. ],
  141. path: ["Algorithm", "Super Micro Bolus (SMB)"]
  142. ),
  143. SettingItem(
  144. title: "Dynamic Settings",
  145. view: .dynamicISF,
  146. searchContents: [
  147. "Dynamic ISF",
  148. "Sigmoid",
  149. "Logarithmic",
  150. "Adjustment Factor (AF)",
  151. "Sigmoid Adjustment Factor",
  152. "Weighted Average of TDD",
  153. "Adjust Basal"
  154. ],
  155. path: ["Algorithm", "Dynamic Sensitivity"]
  156. ),
  157. SettingItem(
  158. title: "Target Behavior",
  159. view: .targetBehavior,
  160. searchContents: [
  161. "High Temptarget Raises Sensitivity",
  162. "Low Temptarget Lowers Sensitivity",
  163. "Sensitivity Raises Target",
  164. "Resistance Lowers Target",
  165. "Half Basal Exercise Target"
  166. ],
  167. scrollTargetLabels: [
  168. "High Temptarget Raises Sensitivity": "High Temp Target Raises Sensitivity",
  169. "Low Temptarget Lowers Sensitivity": "Low Temp Target Lowers Sensitivity"
  170. ],
  171. path: ["Algorithm", "Target Behavior"]
  172. ),
  173. SettingItem(
  174. title: "Additionals",
  175. view: .algorithmAdvancedSettings,
  176. searchContents: [
  177. "Max Daily Safety Multiplier",
  178. "Current Basal Safety Multiplier",
  179. "Use Custom Peak Time",
  180. "Duration of Insulin Action", "DIA",
  181. "Insulin Peak Time",
  182. "Skip Neutral Temps",
  183. "Unsuspend If No Temp",
  184. "SMB Delivery Ratio",
  185. "SMB Interval",
  186. "Min 5m Carbimpact",
  187. "Remaining Carbs Fraction",
  188. "Remaining Carbs Cap",
  189. "Noisy CGM Target Multiplier"
  190. ],
  191. scrollTargetLabels: [
  192. "Min 5m Carbimpact": "Min 5m Carb Impact",
  193. "Remaining Carbs Fraction": "Remaining Carbs Percentage",
  194. "Noisy CGM Target Multiplier": "Noisy CGM Target Increase"
  195. ],
  196. path: ["Algorithm", "Additionals"]
  197. )
  198. ]
  199. static let trioFeaturesItems = [
  200. SettingItem(
  201. title: "Bolus Calculator",
  202. view: .bolusCalculatorConfig,
  203. searchContents: [
  204. "Display Meal Presets",
  205. "Recommended Bolus Percentage",
  206. "Enable Reduced Bolus Factor",
  207. "Reduced Bolus Factor",
  208. "Enable Super Bolus",
  209. "Super Bolus Factor",
  210. "Very Low Glucose Warning"
  211. ],
  212. scrollTargetLabels: [
  213. "Enable Reduced Bolus Factor": "Enable Reduced Bolus Option",
  214. "Reduced Bolus Factor": "Enable Reduced Bolus Option",
  215. "Enable Super Bolus": "Enable Super Bolus Option",
  216. "Super Bolus Factor": "Enable Super Bolus Option"
  217. ],
  218. path: ["Features", "Bolus Calculator"]
  219. ),
  220. SettingItem(
  221. title: "Meal Settings",
  222. view: .mealSettings,
  223. searchContents: [
  224. "Max Carbs",
  225. "Max Meal Absorption Time",
  226. "Max Fat",
  227. "Max Protein",
  228. "Enable Fat and Protein Entries",
  229. "Fat and Protein Delay",
  230. "Spread Interval",
  231. "Fat and Protein Percentage",
  232. "FPU"
  233. ],
  234. scrollTargetLabels: [
  235. "Max Fat": "Enable Fat and Protein Entries",
  236. "Max Protein": "Enable Fat and Protein Entries",
  237. "Fat and Protein Delay": "Enable Fat and Protein Entries",
  238. "Spread Interval": "Enable Fat and Protein Entries",
  239. "Fat and Protein Percentage": "Enable Fat and Protein Entries",
  240. "FPU": "Enable Fat and Protein Entries"
  241. ],
  242. path: ["Features", "Meal Settings"]
  243. ),
  244. SettingItem(
  245. title: "Shortcuts",
  246. view: .shortcutsConfig,
  247. searchContents: ["Allow Bolusing with Shortcuts"],
  248. path: ["Features", "Shortcuts"]
  249. ),
  250. SettingItem(
  251. title: "Remote Control",
  252. view: .remoteControlConfig,
  253. searchContents: ["Remote Control"],
  254. path: ["Features", "Remote Control"]
  255. ),
  256. SettingItem(
  257. title: "User Interface",
  258. view: .userInterfaceSettings,
  259. searchContents: [
  260. "Show X-Axis Grid Lines",
  261. "Show Y-Axis Grid Lines",
  262. "Show Low and High Thresholds",
  263. "Low Threshold",
  264. "High Threshold",
  265. "eA1c/GMI Display Unit",
  266. "Show Carbs Required Badge",
  267. "Carbs Required Threshold",
  268. "Forecast Display Type",
  269. "Bolus Display Threshold",
  270. "Cone",
  271. "Lines",
  272. "Appearance",
  273. "Dark Mode",
  274. "Light Mode",
  275. "Dark Scheme",
  276. "Light Scheme",
  277. "Glucose Color Scheme",
  278. "Time in Range Type",
  279. "Time in Tight Range (TITR)",
  280. "Time in Normoglycemia (TING)",
  281. "X-Axis Interval Step"
  282. ],
  283. scrollTargetLabels: [
  284. "Show Y-Axis Grid Lines": "Show X-Axis Grid Lines",
  285. "High Threshold": "Low Threshold",
  286. "Cone": "Forecast Display Type",
  287. "Lines": "Forecast Display Type",
  288. "Dark Mode": "Appearance",
  289. "Light Mode": "Appearance",
  290. "Time in Tight Range (TITR)": "Time in Range Type",
  291. "Time in Normoglycemia (TING)": "Time in Range Type",
  292. "Dark Scheme": "Appearance",
  293. "Light Scheme": "Appearance",
  294. "X-Axis Interval Step": "Show X-Axis Grid Lines",
  295. "Carbs Required Threshold": "Show Carbs Required Badge"
  296. ],
  297. path: ["Features", "User Interface"]
  298. ),
  299. SettingItem(
  300. title: "App Icons",
  301. view: .iconConfig,
  302. searchContents: ["Trio Icon"],
  303. path: ["Features", "App Icons"]
  304. ),
  305. SettingItem(
  306. title: "App Diagnostics",
  307. view: .appDiagnostics,
  308. searchContents: ["Anonymized Data Sharing"],
  309. path: ["Features", "App Diagnostics"]
  310. )
  311. ]
  312. static let notificationItems = [
  313. SettingItem(title: "Manage iOS Preferences", view: .notificationSettings),
  314. SettingItem(
  315. title: "Trio Notifications",
  316. view: .glucoseNotificationSettings,
  317. searchContents: [
  318. "Always Notify Pump",
  319. "Always Notify CGM",
  320. "Always Notify Carb",
  321. "Always Notify Algorithm",
  322. "Show Glucose App Badge",
  323. "Glucose Notifications",
  324. "Add Glucose Source to Alarm",
  325. "Low Glucose Alarm Limit",
  326. "High Glucose Alarm Limit"
  327. ],
  328. scrollTargetLabels: [
  329. "Low Glucose Alarm Limit": "Glucose Notifications",
  330. "High Glucose Alarm Limit": "Glucose Notifications"
  331. ],
  332. path: ["Notifications", "Trio Notifications"] // Glucose
  333. ),
  334. SettingItem(
  335. title: "Live Activity",
  336. view: .liveActivitySettings,
  337. searchContents: [
  338. "Enable Live Activity",
  339. "Lock Screen Widget Style"
  340. ],
  341. path: ["Notifications", "Live Activity"]
  342. ),
  343. SettingItem(
  344. title: "Calendar Events",
  345. view: .calendarEventSettings,
  346. searchContents: [
  347. "Create Calendar Events",
  348. "Choose Calendar",
  349. "Display Emojis as Labels",
  350. "Display IOB and COB"
  351. ],
  352. path: ["Notifications", "Calendar Events"]
  353. )
  354. ]
  355. static let serviceItems = [
  356. SettingItem(
  357. title: "Nightscout",
  358. view: .nighscoutConfig,
  359. searchContents: [
  360. "Import Settings",
  361. "Backfill Glucose"
  362. ],
  363. path: ["Services", "Nightscout"]
  364. ),
  365. SettingItem(
  366. title: "Nightscout Upload",
  367. view: .nighscoutConfig,
  368. searchContents: [
  369. "Allow Uploading to Nightscout",
  370. "Upload Glucose"
  371. ],
  372. path: ["Services", "Nightscout", "Upload"]
  373. ),
  374. SettingItem(
  375. title: "Nightscout Fetch & Remote Control",
  376. view: .nighscoutConfig,
  377. searchContents: [
  378. "Allow Fetching From Nightscout"
  379. ],
  380. path: ["Services", "Nightscout", "Fetch and Remote Control"]
  381. ),
  382. SettingItem(title: "Tidepool", view: .serviceSettings, path: ["Services"]),
  383. SettingItem(title: "Apple Health", view: .healthkit, path: ["Services"])
  384. ]
  385. static var allItems: [SettingItem] {
  386. trioConfig + devicesItems + therapyItems + algorithmItems + trioFeaturesItems + notificationItems + serviceItems
  387. }
  388. static func filteredItems(searchText: String) -> [FilteredSettingItem] {
  389. allItems.flatMap { item in
  390. var results = [FilteredSettingItem]()
  391. let searchLower = searchText.lowercased()
  392. let titleLocalized = item.title.localized
  393. let titleEnglish = item.title.englishLocalized
  394. if titleLocalized.localizedCaseInsensitiveContains(searchLower) ||
  395. titleEnglish.localizedCaseInsensitiveContains(searchLower)
  396. {
  397. results.append(FilteredSettingItem(settingItem: item, matchedContent: item.title))
  398. }
  399. if let contents = item.searchContents {
  400. let matched = contents.filter {
  401. $0.localized.localizedCaseInsensitiveContains(searchLower) ||
  402. $0.englishLocalized.localizedCaseInsensitiveContains(searchLower)
  403. }
  404. results.append(contentsOf: matched.map { FilteredSettingItem(settingItem: item, matchedContent: $0) })
  405. }
  406. return results
  407. }
  408. }
  409. }
  410. extension String {
  411. func localizedString(locale: Locale = .current) -> String {
  412. if locale.identifier == "en",
  413. let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
  414. let bundle = Bundle(path: path)
  415. {
  416. return NSLocalizedString(self, bundle: bundle, comment: "")
  417. }
  418. return NSLocalizedString(self, comment: "")
  419. }
  420. var localized: String { localizedString() }
  421. var englishLocalized: String { localizedString(locale: Locale(identifier: "en")) }
  422. }