Pārlūkot izejas kodu

feat: Display latest dev version in Settings for non-main builds

Add display of latest dev version (x.x.x.x format) in Settings for all non-main branch builds.
Shows version with visual indicators for update status. Uses same
caching and comparison logic as main version checking.

- Only visible when not on main branch builds
- Orange color with arrow icon when newer version available
- Secondary color with hammer icon when on latest version
- 24-hour cache to minimize API calls
- Properly parses APP_DEV_VERSION for x.x.x.x format
- Added localization entries for new strings
Sjoerd Bozon 1 gadu atpakaļ
vecāks
revīzija
21bd82e8d6

+ 7 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -125293,6 +125293,12 @@
         }
       }
     },
+    "Latest dev: %@" : {
+
+    },
+    "Latest dev: Fetching..." : {
+
+    },
     "Latest Raw Algorithm Output" : {
       "localizations" : {
         "bg" : {
@@ -243195,4 +243201,4 @@
     }
   },
   "version" : "1.0"
-}
+}

+ 51 - 16
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -9,6 +9,8 @@ extension Settings {
         var latestVersion: String?
         var isUpdateAvailable: Bool
         var isBlacklisted: Bool
+        var latestDevVersion: String?
+        var isDevUpdateAvailable: Bool
     }
 
     struct RootView: BaseView {
@@ -27,7 +29,9 @@ extension Settings {
         @State private var versionInfo = VersionInfo(
             latestVersion: nil,
             isUpdateAvailable: false,
-            isBlacklisted: false
+            isBlacklisted: false,
+            latestDevVersion: nil,
+            isDevUpdateAvailable: false
         )
         @State private var closedLoopDisabled = true
 
@@ -40,12 +44,13 @@ extension Settings {
         }
 
         @ViewBuilder var versionInfoView: some View {
-            let latestVersion = versionInfo.latestVersion
-            if let version = latestVersion {
-                let updateColor: Color = versionInfo.isUpdateAvailable ? .orange : .green
-                let versionIconName = versionInfo.isUpdateAvailable ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
+            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"
 
-                VStack(alignment: .leading, spacing: 4) {
                     HStack {
                         Text("Latest version: \(version)")
                             .font(.footnote)
@@ -62,11 +67,33 @@ extension Settings {
                                 .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)
+                    }
                 }
-            } else {
-                Text("Latest version: Fetching...")
-                    .font(.footnote)
-                    .foregroundColor(.secondary)
             }
         }
 
@@ -367,13 +394,21 @@ extension Settings {
             .screenNavigation(self)
             .onAppear {
                 AppVersionChecker.shared.refreshVersionInfo { _, latestVersion, isNewer, isBlacklisted in
-                    let updateAvailable = isNewer
                     DispatchQueue.main.async {
-                        versionInfo = VersionInfo(
-                            latestVersion: latestVersion,
-                            isUpdateAvailable: updateAvailable,
-                            isBlacklisted: isBlacklisted
-                        )
+                        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" {
+                    AppVersionChecker.shared.checkForNewDevVersion { devVersion, isDevNewer in
+                        DispatchQueue.main.async {
+                            versionInfo.latestDevVersion = devVersion
+                            versionInfo.isDevUpdateAvailable = isDevNewer
+                        }
                     }
                 }
             }

+ 115 - 1
Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

@@ -27,12 +27,22 @@ final class AppVersionChecker {
     /// Timestamp for the last time an expiration notification was shown.
     @Persisted(key: "lastExpirationNotificationShown") private var lastExpirationNotificationShown: Date? = .distantPast
 
+    // Dev version properties
+    /// Cached app version for which dev data was last fetched.
+    @Persisted(key: "cachedForDevVersion") private var cachedForDevVersion: String? = nil
+    /// The latest dev version fetched from GitHub.
+    @Persisted(key: "latestDevVersion") private var persistedLatestDevVersion: String? = nil
+    /// The date when the latest dev version was checked.
+    @Persisted(key: "latestDevVersionChecked") private var latestDevVersionChecked: Date? = .distantPast
+
     // MARK: - Nested Types
 
     /// GitHubDataType defines the type of data to fetch from GitHub for version checking.
     private enum GitHubDataType {
         /// The configuration file containing version information.
         case versionConfig
+        /// The configuration file containing dev version information.
+        case devVersionConfig
         /// The JSON file listing blacklisted versions.
         case blacklistedVersions
 
@@ -41,6 +51,8 @@ final class AppVersionChecker {
             switch self {
             case .versionConfig:
                 return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/Config.xcconfig"
+            case .devVersionConfig:
+                return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/dev/Config.xcconfig"
             case .blacklistedVersions:
                 return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/blacklisted-versions.json"
             }
@@ -135,6 +147,84 @@ final class AppVersionChecker {
         }
     }
 
+    /**
+     Checks for the latest dev version with caching and comparison.
+
+     This method attempts to use cached dev version data if it is less than 24 hours old and
+     corresponds to the current app version. If the cache is invalid or outdated,
+     it fetches fresh data from GitHub.
+
+     - Parameter completion: A closure that receives:
+     - latestDevVersion: The latest dev version string (if available).
+     - isNewer: `true` if the fetched dev version is newer than the current version.
+     */
+    func checkForNewDevVersion(completion: @escaping (String?, Bool) -> Void) {
+        // For dev version, we need to compare against the current dev version, not the main version
+        let currentDevVersion = Bundle.main.object(forInfoDictionaryKey: "AppDevVersion") as? String ?? version()
+        let now = Date()
+
+        // Retrieve cached values
+        let lastChecked = latestDevVersionChecked ?? .distantPast
+        let cachedVersion = cachedForDevVersion
+        let persistedLatestDev = persistedLatestDevVersion
+
+        // Use cached data if it is valid (less than 24 hours old) and matches the current version
+        if let cachedVersion = cachedVersion,
+           cachedVersion == currentDevVersion,
+           now.timeIntervalSince(lastChecked) < 24 * 3600,
+           let persistedLatestDev = persistedLatestDev
+        {
+            let isNewer = isVersion(persistedLatestDev, newerThan: currentDevVersion)
+            completion(persistedLatestDev, isNewer)
+            return
+        }
+
+        // Otherwise, fetch fresh data from GitHub and update the cache
+        fetchDevVersionAndUpdateCache(currentVersion: currentDevVersion, completion: completion)
+    }
+
+    /**
+     Fetches dev version data from GitHub, updates persisted values, and invokes the completion handler.
+
+     - Parameters:
+     - currentVersion: The current app version.
+     - completion: A closure that receives:
+     - latestDevVersion: The latest dev version string from GitHub (if available).
+     - isNewer: `true` if the fetched dev version is newer than the current version.
+     */
+    private func fetchDevVersionAndUpdateCache(currentVersion: String, completion: @escaping (String?, Bool) -> Void) {
+        fetchData(for: .devVersionConfig) { versionData in
+            DispatchQueue.main.async {
+                // Parse the dev version from the fetched configuration data
+                let configContents = versionData.flatMap { String(data: $0, encoding: .utf8) }
+                let fetchedDevVersion = configContents.flatMap { self.parseDevVersionFromConfig(contents: $0) }
+
+                #if DEBUG
+                    print("AppVersionChecker.fetchDevVersion: Current dev version: \(currentVersion)")
+                    print("AppVersionChecker.fetchDevVersion: Fetched dev version: \(fetchedDevVersion ?? "nil")")
+                    if let contents = configContents {
+                        let lines = contents.split(separator: "\n")
+                        for line in lines where line.contains("VERSION") {
+                            print("AppVersionChecker.fetchDevVersion: Config line: \(line)")
+                        }
+                    }
+                #endif
+
+                // Determine if the fetched dev version is newer than the current version
+                let isNewer = fetchedDevVersion.map {
+                    self.isVersion($0, newerThan: currentVersion)
+                } ?? false
+
+                // Update persisted cache
+                self.persistedLatestDevVersion = fetchedDevVersion
+                self.latestDevVersionChecked = Date()
+                self.cachedForDevVersion = currentVersion
+
+                completion(fetchedDevVersion, isNewer)
+            }
+        }
+    }
+
     // MARK: - Core Version Checking Logic
 
     /**
@@ -271,7 +361,31 @@ final class AppVersionChecker {
     private func parseVersionFromConfig(contents: String) -> String? {
         let lines = contents.split(separator: "\n")
         for line in lines {
-            if line.contains("APP_VERSION") {
+            if line.contains("APP_VERSION"), !line.contains("APP_DEV_VERSION") {
+                let components = line.split(separator: "=").map {
+                    $0.trimmingCharacters(in: .whitespacesAndNewlines)
+                }
+                if components.count > 1 {
+                    return components[1]
+                }
+            }
+        }
+        return nil
+    }
+
+    /**
+     Parses the dev version string from the contents of a configuration file.
+
+     The method scans each line of the provided content for an occurrence of "APP_DEV_VERSION" and then
+     extracts the version number following the "=" delimiter.
+
+     - Parameter contents: A string containing the contents of the configuration file.
+     - Returns: The extracted dev version string if found; otherwise, `nil`.
+     */
+    private func parseDevVersionFromConfig(contents: String) -> String? {
+        let lines = contents.split(separator: "\n")
+        for line in lines {
+            if line.contains("APP_DEV_VERSION") {
                 let components = line.split(separator: "=").map {
                     $0.trimmingCharacters(in: .whitespacesAndNewlines)
                 }