import HealthKit import LoopKit import LoopKitUI import SwiftUI import Swinject import UIKit extension Settings { struct VersionInfo: Equatable { var latestVersion: String? var isUpdateAvailable: Bool var isBlacklisted: Bool var latestDevVersion: String? var isDevUpdateAvailable: Bool } struct RootView: BaseView { let resolver: Resolver @StateObject var state = StateModel() @State private var showShareSheet = false @State private var searchText: String = "" @State private var shouldDisplayHint: Bool = false @State var hintDetent = PresentationDetent.large @State var selectedVerboseHint: AnyView? @State var hintLabel: String? @State private var decimalPlaceholder: Decimal = 0.0 @State private var booleanPlaceholder: Bool = false @State private var versionInfo = VersionInfo( latestVersion: nil, isUpdateAvailable: false, isBlacklisted: false, latestDevVersion: nil, isDevUpdateAvailable: false ) @State private var closedLoopDisabled = true @State private var showCopiedToast = false @Environment(\.colorScheme) var colorScheme @EnvironmentObject var appIcons: Icons @Environment(AppState.self) var appState @Environment(SettingsSearchHighlight.self) var searchHighlight private var filteredItems: [FilteredSettingItem] { SettingItems.filteredItems(searchText: searchText) } @ViewBuilder var versionInfoView: some View { VStack(alignment: .leading, spacing: 4) { // Main version info if let version = versionInfo.latestVersion { let updateColor: Color = versionInfo.isUpdateAvailable ? .orange : .green let versionIconName = versionInfo .isUpdateAvailable ? "exclamationmark.triangle.fill" : "checkmark.circle.fill" HStack { Text("Latest version: \(version)") .font(.footnote) .foregroundColor(updateColor) Image(systemName: versionIconName) .foregroundColor(updateColor) } if versionInfo.isBlacklisted { HStack { Text("Warning: Known issues. Update now.") .font(.footnote) .foregroundColor(.red) Image(systemName: "exclamationmark.octagon.fill") .foregroundColor(.red) } } } else { Text("Latest version: Fetching...") .font(.footnote) .foregroundColor(.secondary) } // Show latest dev version on any branch except main let buildDetails = BuildDetails.shared if buildDetails.trioBranch != "main" { if let devVersion = versionInfo.latestDevVersion { let devUpdateColor: Color = versionInfo.isDevUpdateAvailable ? .orange : .secondary let devVersionIconName = versionInfo.isDevUpdateAvailable ? "arrow.up.circle.fill" : "hammer.fill" HStack { Text("Latest dev: \(devVersion)") .font(.footnote) .foregroundColor(devUpdateColor) Image(systemName: devVersionIconName) .font(.footnote) .foregroundColor(devUpdateColor) } } else { Text("Latest dev: Fetching...") .font(.footnote) .foregroundColor(.secondary) } } } } var body: some View { List { if searchText.isEmpty { let buildDetails = BuildDetails.shared Section( header: Text("BRANCH: \(buildDetails.branchAndSha)").textCase(nil), content: { /// The current development version of the app. /// /// Follows a semantic pattern where release versions are like `0.5.0`, and /// development versions increment with a fourth component (e.g., `0.5.0.1`, `0.5.0.2`) /// after the base release. For example: /// - After release `0.5.0` → `0.5.0` /// - First dev push → `0.5.0.1` /// - Next dev push → `0.5.0.2` /// - Next release `0.6.0` → `0.6.0` /// - Next dev push → `0.6.0.1` /// /// If the dev version is unavailable, `"unknown"` is returned. let devVersion = Bundle.main.appDevVersion ?? "unknown" let buildNumber = Bundle.main.buildVersionNumber ?? String(localized: "Unknown") NavigationLink(destination: SubmodulesView(buildDetails: buildDetails)) { HStack { Image(appIcons.appIcon.rawValue) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 50, height: 50) .cornerRadius(10) .padding(.trailing, 10) VStack(alignment: .leading, spacing: 4) { Text("Trio v\(devVersion) (\(buildNumber))") .font(.headline) if let expirationDate = buildDetails.calculateExpirationDate() { let formattedDate = DateFormatter.localizedString( from: expirationDate, dateStyle: .medium, timeStyle: .none ) Text("\(buildDetails.expirationHeaderString): \(formattedDate)") .font(.footnote) .foregroundColor(.secondary) } else { Text("Simulator Build has no expiry") .font(.footnote) .foregroundColor(.secondary) } versionInfoView } } .contentShape(Rectangle()) .onLongPressGesture { UIPasteboard.general.string = "Trio v\(devVersion) (\(buildNumber)) \(buildDetails.branchAndSha)" UINotificationFeedbackGenerator().notificationOccurred(.success) withAnimation { showCopiedToast = true } DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { showCopiedToast = false } } } } } ).listRowBackground(Color.chart) let miniHintText = closedLoopDisabled ? String(localized: "Add a CGM and pump to enable automated insulin delivery") : String(localized: "Enable automated insulin delivery.") let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange : .accentColor let miniHintTextColor: Color = closedLoopDisabled ? miniHintTextColorForDisabled : .secondary SettingInputSection( decimalValue: $decimalPlaceholder, booleanValue: $state.closedLoop, shouldDisplayHint: $shouldDisplayHint, selectedVerboseHint: Binding( get: { selectedVerboseHint }, set: { selectedVerboseHint = $0.map { AnyView($0) } hintLabel = String(localized: "Closed Loop") } ), units: state.units, type: .boolean, label: String(localized: "Closed Loop"), miniHint: miniHintText, verboseHint: VStack(alignment: .leading, spacing: 10) { Text( "Running Trio in closed loop mode requires an active CGM sensor session and a connected pump. This enables automated insulin delivery." ) Text( "Before enabling, dial in your settings (basal / insulin sensitivity / carb ratio), and familiarize yourself with the app." ) }, headerText: String(localized: "Automated Insulin Delivery"), isToggleDisabled: closedLoopDisabled, miniHintColor: miniHintTextColor ) .onAppear { closedLoopDisabled = !state.hasCgmAndPump() } Section( header: Text("Trio Configuration"), content: { ForEach(SettingItems.trioConfig) { item in Text(LocalizedStringKey(item.title)).navigationLink(to: item.view, from: self) } } ) .listRowBackground(Color.chart) Section( header: Text("Support & Community"), content: { Button { showShareSheet.toggle() } label: { HStack { Text("Share Logs") .foregroundColor(.primary) Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.footnote) } } .frame(maxWidth: .infinity, alignment: .leading) Button { if let url = URL(string: "https://github.com/nightscout/Trio/issues/new/choose") { UIApplication.shared.open(url) } } label: { HStack { Text("Submit Ticket on GitHub") .foregroundColor(.primary) Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.footnote) } } .frame(maxWidth: .infinity, alignment: .leading) Button { if let url = URL(string: "https://discord.triodocs.org") { UIApplication.shared.open(url) } } label: { HStack { Text("Trio Discord") .foregroundColor(.primary) Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.footnote) } } .frame(maxWidth: .infinity, alignment: .leading) Button { if let url = URL(string: "https://facebook.triodocs.org") { UIApplication.shared.open(url) } } label: { HStack { Text("Trio Facebook") .foregroundColor(.primary) Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.footnote) } } .frame(maxWidth: .infinity, alignment: .leading) } ).listRowBackground(Color.chart) Section( header: Text("Trio Backup"), content: { Text(String( localized: "Export Settings", comment: "Export Settings menu item in Trio Settings Root View" )) .navigationLink(to: .settingsExport, from: self) } ).listRowBackground(Color.chart) } else { Section( header: Text("Search Results"), content: { if filteredItems.isNotEmpty { ForEach(filteredItems) { filteredItem in NavigationLink(value: SearchResultTarget( screen: filteredItem.settingItem.view, scrollLabel: filteredItem.scrollLabel.localized )) { VStack(alignment: .leading) { Text(filteredItem.matchedContent.localized).bold() if let path = filteredItem.settingItem.path { Text(path.map(\.localized).joined(separator: " > ")) .font(.caption) .foregroundColor(.secondary) } } } } } else { Text("No settings matching your search query") + Text(" »\(searchText)« ").bold() + Text("found.") } } ).listRowBackground(Color.chart) } } .overlay(alignment: .bottom) { if showCopiedToast { Label("Copied", systemImage: "checkmark.circle.fill") .font(.footnote.weight(.semibold)) .foregroundColor(.white) .padding(.horizontal, 16) .padding(.vertical, 10) .background(.ultraThinMaterial, in: Capsule()) .padding(.bottom, 32) .transition(.move(edge: .bottom).combined(with: .opacity)) } } .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme)) .sheet(isPresented: $shouldDisplayHint) { SettingInputHintView( hintDetent: $hintDetent, shouldDisplayHint: $shouldDisplayHint, hintLabel: hintLabel ?? "", hintText: selectedVerboseHint ?? AnyView(EmptyView()), sheetTitle: String(localized: "Help", comment: "Help sheet title") ) } .sheet(isPresented: $showShareSheet) { ShareSheet(activityItems: state.logItems()) } .onAppear(perform: configureView) .navigationTitle("Settings") .navigationBarTitleDisplayMode(.automatic) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button( action: { if let url = URL(string: "https://triodocs.org/") { UIApplication.shared.open(url) } }, label: { HStack { Text("Trio Docs") Image(systemName: "questionmark.circle") } } ) } } .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) .navigationDestination(for: SearchResultTarget.self) { target in state.view(for: target.screen) .onAppear { searchHighlight.highlightedSetting = target.scrollLabel } } .screenNavigation(self) .onAppear { Task { @MainActor in let (_, latestVersion, isNewer, isBlacklisted) = await AppVersionChecker.shared.refreshVersionInfo() versionInfo.latestVersion = latestVersion versionInfo.isUpdateAvailable = isNewer versionInfo.isBlacklisted = isBlacklisted // Fetch dev version if not on main branch let buildDetails = BuildDetails.shared if buildDetails.trioBranch != "main" { let (devVersion, isDevNewer) = await AppVersionChecker.shared.checkForNewDevVersion() versionInfo.latestDevVersion = devVersion versionInfo.isDevUpdateAvailable = isDevNewer } } } } } }