ImportExportSettingsView.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // LoopFollow
  2. // ImportExportSettingsView.swift
  3. import AVFoundation
  4. import SwiftUI
  5. import UIKit
  6. struct ImportExportSettingsView: View {
  7. @StateObject private var viewModel = ImportExportSettingsViewModel()
  8. var body: some View {
  9. NavigationView {
  10. List {
  11. // MARK: - Import Section
  12. Section("Import Settings") {
  13. Button(action: {
  14. viewModel.isShowingQRCodeScanner = true
  15. }) {
  16. HStack {
  17. Image(systemName: "qrcode.viewfinder")
  18. .foregroundColor(.blue)
  19. Text("Scan QR Code to Import Settings")
  20. }
  21. }
  22. .buttonStyle(.plain)
  23. }
  24. // MARK: - Export Section
  25. Section("Export Settings To QR Code") {
  26. ForEach(ImportExportSettingsViewModel.ExportType.allCases, id: \.self) { exportType in
  27. Button(action: {
  28. if exportType == .alarms {
  29. viewModel.showAlarmSelection()
  30. } else {
  31. viewModel.exportType = exportType
  32. if let qrString = viewModel.generateQRCodeForExport() {
  33. viewModel.qrCodeString = qrString
  34. viewModel.isShowingQRCodeDisplay = true
  35. }
  36. }
  37. }) {
  38. HStack {
  39. Image(systemName: exportType.icon)
  40. .foregroundColor(.blue)
  41. Text("Export \(exportType.rawValue)")
  42. Spacer()
  43. Image(systemName: exportType == .alarms ? "list.bullet" : "qrcode")
  44. .foregroundColor(.secondary)
  45. }
  46. }
  47. .buttonStyle(.plain)
  48. }
  49. }
  50. // MARK: - iCloud Section
  51. Section("iCloud Import") {
  52. Button(action: {
  53. viewModel.importFromiCloud()
  54. }) {
  55. HStack {
  56. Image(systemName: "icloud.and.arrow.down")
  57. .foregroundColor(.green)
  58. Text("Import Settings from iCloud")
  59. }
  60. }
  61. .buttonStyle(.plain)
  62. }
  63. Section("iCloud Export") {
  64. Button(action: {
  65. viewModel.exportToiCloud()
  66. }) {
  67. HStack {
  68. Image(systemName: "icloud.and.arrow.up")
  69. .foregroundColor(.blue)
  70. Text("Export All Settings to iCloud")
  71. }
  72. }
  73. .buttonStyle(.plain)
  74. }
  75. // MARK: - Status Message
  76. if !viewModel.qrCodeErrorMessage.isEmpty {
  77. Section {
  78. let isSuccess = viewModel.qrCodeErrorMessage.contains("successfully") || viewModel.qrCodeErrorMessage.contains("Successfully imported")
  79. let displayText = isSuccess ? "✅ \(viewModel.qrCodeErrorMessage)" : viewModel.qrCodeErrorMessage
  80. Text(displayText)
  81. .foregroundColor(isSuccess ? .green : .red)
  82. .font(.caption)
  83. }
  84. }
  85. }
  86. .navigationTitle("Import/Export Settings")
  87. .navigationBarTitleDisplayMode(.inline)
  88. }
  89. .sheet(isPresented: $viewModel.isShowingQRCodeScanner) {
  90. SimpleQRCodeScannerView { result in
  91. viewModel.handleQRCodeScanResult(result)
  92. }
  93. }
  94. .sheet(isPresented: $viewModel.isShowingQRCodeDisplay) {
  95. NavigationView {
  96. VStack {
  97. if !viewModel.qrCodeString.isEmpty {
  98. QRCodeDisplayView(
  99. qrCodeString: viewModel.qrCodeString,
  100. size: CGSize(width: 300, height: 300)
  101. )
  102. .padding()
  103. Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.rawValue.lowercased())")
  104. .font(.caption)
  105. .foregroundColor(.secondary)
  106. .multilineTextAlignment(.center)
  107. .padding(.horizontal, 20)
  108. } else {
  109. Text("Failed to generate QR code")
  110. .foregroundColor(.red)
  111. .padding()
  112. }
  113. }
  114. .navigationTitle("Export \(viewModel.exportType.rawValue)")
  115. .navigationBarTitleDisplayMode(.inline)
  116. .navigationBarItems(trailing: Button("Done") {
  117. viewModel.isShowingQRCodeDisplay = false
  118. })
  119. }
  120. }
  121. .sheet(isPresented: $viewModel.isShowingAlarmSelection) {
  122. AlarmSelectionView(
  123. exportedAlarmIds: viewModel.exportedAlarmIds,
  124. onConfirm: { selectedAlarms in
  125. viewModel.exportSelectedAlarms(selectedAlarms)
  126. },
  127. onCancel: {
  128. viewModel.cancelAlarmSelection()
  129. }
  130. )
  131. }
  132. .onDisappear {
  133. viewModel.resetExportedAlarms()
  134. }
  135. .sheet(isPresented: $viewModel.showImportConfirmation) {
  136. ImportConfirmationView(viewModel: viewModel)
  137. }
  138. .sheet(isPresented: $viewModel.showExportSuccessAlert) {
  139. ExportSuccessView(viewModel: viewModel)
  140. }
  141. .sheet(isPresented: $viewModel.showImportNotFoundAlert) {
  142. ImportNotFoundView(viewModel: viewModel)
  143. }
  144. }
  145. }
  146. struct ImportNotFoundView: View {
  147. @ObservedObject var viewModel: ImportExportSettingsViewModel
  148. var body: some View {
  149. NavigationView {
  150. VStack(spacing: 20) {
  151. // Header
  152. VStack(spacing: 12) {
  153. Image(systemName: "icloud.slash")
  154. .font(.system(size: 60))
  155. .foregroundColor(.orange)
  156. Text("No Settings Found")
  157. .font(.title2)
  158. .fontWeight(.semibold)
  159. Text(viewModel.importNotFoundMessage)
  160. .font(.subheadline)
  161. .foregroundColor(.secondary)
  162. .multilineTextAlignment(.center)
  163. .padding(.horizontal)
  164. }
  165. .padding(.top, 30)
  166. Spacer()
  167. // Done Button
  168. Button(action: {
  169. viewModel.showImportNotFoundAlert = false
  170. }) {
  171. HStack {
  172. Image(systemName: "xmark")
  173. Text("OK")
  174. }
  175. .font(.headline)
  176. .foregroundColor(.white)
  177. .frame(maxWidth: .infinity)
  178. .padding()
  179. .background(Color.blue)
  180. .cornerRadius(12)
  181. }
  182. .padding(.horizontal)
  183. .padding(.bottom, 20)
  184. }
  185. .navigationBarHidden(true)
  186. }
  187. }
  188. }
  189. struct ExportSuccessView: View {
  190. @ObservedObject var viewModel: ImportExportSettingsViewModel
  191. var body: some View {
  192. NavigationView {
  193. VStack(spacing: 20) {
  194. // Header
  195. VStack(spacing: 12) {
  196. Image(systemName: "checkmark.icloud.fill")
  197. .font(.system(size: 60))
  198. .foregroundColor(.green)
  199. Text("Export Successful")
  200. .font(.title2)
  201. .fontWeight(.semibold)
  202. Text(viewModel.exportSuccessMessage)
  203. .font(.subheadline)
  204. .foregroundColor(.secondary)
  205. .multilineTextAlignment(.center)
  206. }
  207. .padding(.top, 30)
  208. // Exported Settings Details
  209. if !viewModel.exportSuccessDetails.isEmpty {
  210. VStack(alignment: .leading, spacing: 16) {
  211. Text("Exported Settings")
  212. .font(.headline)
  213. .padding(.horizontal)
  214. VStack(spacing: 12) {
  215. ForEach(viewModel.exportSuccessDetails, id: \.self) { detail in
  216. HStack(spacing: 12) {
  217. Image(systemName: iconForDetail(detail))
  218. .font(.title2)
  219. .foregroundColor(colorForDetail(detail))
  220. .frame(width: 30)
  221. Text(detail)
  222. .font(.subheadline)
  223. .lineLimit(2)
  224. Spacer()
  225. Image(systemName: "checkmark.circle.fill")
  226. .foregroundColor(.green)
  227. }
  228. .padding()
  229. .background(Color(.systemGray6))
  230. .cornerRadius(10)
  231. }
  232. }
  233. .padding(.horizontal)
  234. }
  235. }
  236. Spacer()
  237. // Done Button
  238. Button(action: {
  239. viewModel.showExportSuccessAlert = false
  240. }) {
  241. HStack {
  242. Image(systemName: "checkmark")
  243. Text("Done")
  244. }
  245. .font(.headline)
  246. .foregroundColor(.white)
  247. .frame(maxWidth: .infinity)
  248. .padding()
  249. .background(Color.blue)
  250. .cornerRadius(12)
  251. }
  252. .padding(.horizontal)
  253. .padding(.bottom, 20)
  254. }
  255. .navigationBarHidden(true)
  256. }
  257. }
  258. private func iconForDetail(_ detail: String) -> String {
  259. if detail.lowercased().contains("nightscout") {
  260. return "network"
  261. } else if detail.lowercased().contains("dexcom") {
  262. return "person.circle"
  263. } else if detail.lowercased().contains("remote") {
  264. return "antenna.radiowaves.left.and.right"
  265. } else if detail.lowercased().contains("alarm") {
  266. return "bell"
  267. }
  268. return "gear"
  269. }
  270. private func colorForDetail(_ detail: String) -> Color {
  271. if detail.lowercased().contains("nightscout") {
  272. return .blue
  273. } else if detail.lowercased().contains("dexcom") {
  274. return .green
  275. } else if detail.lowercased().contains("remote") {
  276. return .orange
  277. } else if detail.lowercased().contains("alarm") {
  278. return .red
  279. }
  280. return .gray
  281. }
  282. }
  283. struct ImportConfirmationView: View {
  284. @ObservedObject var viewModel: ImportExportSettingsViewModel
  285. var body: some View {
  286. NavigationView {
  287. VStack(spacing: 20) {
  288. // Header
  289. VStack(spacing: 8) {
  290. Image(systemName: "square.and.arrow.down")
  291. .font(.system(size: 50))
  292. .foregroundColor(.blue)
  293. Text("Import Settings")
  294. .font(.title2)
  295. .fontWeight(.semibold)
  296. Text("Review the settings that will be imported")
  297. .font(.subheadline)
  298. .foregroundColor(.secondary)
  299. .multilineTextAlignment(.center)
  300. }
  301. .padding(.top, 20)
  302. // Settings Preview
  303. if let preview = viewModel.importPreview {
  304. VStack(alignment: .leading, spacing: 16) {
  305. Text("Settings to Import")
  306. .font(.headline)
  307. .padding(.horizontal)
  308. VStack(spacing: 12) {
  309. if let url = preview.nightscoutURL, !url.isEmpty {
  310. SettingRowView(
  311. icon: "network",
  312. title: "Nightscout URL",
  313. value: url,
  314. color: .blue
  315. )
  316. }
  317. if let username = preview.dexcomUsername, !username.isEmpty {
  318. SettingRowView(
  319. icon: "person.circle",
  320. title: "Dexcom Username",
  321. value: username,
  322. color: .green
  323. )
  324. }
  325. if let remoteType = preview.remoteType, !remoteType.isEmpty, remoteType != "None" {
  326. SettingRowView(
  327. icon: "antenna.radiowaves.left.and.right",
  328. title: "Remote Type",
  329. value: remoteType,
  330. color: .orange
  331. )
  332. }
  333. if preview.alarmCount > 0 {
  334. SettingRowView(
  335. icon: "bell",
  336. title: "Alarms",
  337. value: "\(preview.alarmCount) alarm(s): \(preview.alarmNames.joined(separator: ", "))",
  338. color: .red
  339. )
  340. }
  341. }
  342. .padding(.horizontal)
  343. }
  344. }
  345. // Warning
  346. VStack(spacing: 8) {
  347. HStack {
  348. Image(systemName: "exclamationmark.triangle")
  349. .foregroundColor(.orange)
  350. Text("Warning")
  351. .fontWeight(.semibold)
  352. .foregroundColor(.orange)
  353. }
  354. Text("This will overwrite your current settings")
  355. .font(.subheadline)
  356. .foregroundColor(.secondary)
  357. .multilineTextAlignment(.center)
  358. }
  359. .padding(.horizontal)
  360. Spacer()
  361. // Action Buttons
  362. VStack(spacing: 12) {
  363. Button(action: {
  364. viewModel.confirmImport()
  365. }) {
  366. HStack {
  367. Image(systemName: "checkmark")
  368. Text("Import Settings")
  369. }
  370. .font(.headline)
  371. .foregroundColor(.white)
  372. .frame(maxWidth: .infinity)
  373. .padding()
  374. .background(Color.blue)
  375. .cornerRadius(12)
  376. }
  377. Button(action: {
  378. viewModel.cancelImport()
  379. }) {
  380. HStack {
  381. Image(systemName: "xmark")
  382. Text("Cancel")
  383. }
  384. .font(.headline)
  385. .foregroundColor(.primary)
  386. .frame(maxWidth: .infinity)
  387. .padding()
  388. .background(Color(.systemGray6))
  389. .cornerRadius(12)
  390. }
  391. }
  392. .padding(.horizontal)
  393. .padding(.bottom, 20)
  394. }
  395. .navigationBarHidden(true)
  396. }
  397. }
  398. }
  399. struct SettingRowView: View {
  400. let icon: String
  401. let title: String
  402. let value: String
  403. let color: Color
  404. var body: some View {
  405. HStack(spacing: 12) {
  406. Image(systemName: icon)
  407. .font(.title2)
  408. .foregroundColor(color)
  409. .frame(width: 30)
  410. VStack(alignment: .leading, spacing: 2) {
  411. Text(title)
  412. .font(.subheadline)
  413. .fontWeight(.medium)
  414. Text(value)
  415. .font(.caption)
  416. .foregroundColor(.secondary)
  417. .lineLimit(2)
  418. }
  419. Spacer()
  420. }
  421. .padding()
  422. .background(Color(.systemGray6))
  423. .cornerRadius(10)
  424. }
  425. }
  426. #Preview {
  427. ImportExportSettingsView()
  428. }