Explorar el Código

Menu refactoring

Jonas Björkert hace 4 meses
padre
commit
f84cc2f71e

+ 94 - 14
LoopFollow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,16 +1,96 @@
 {
-  "object": {
-    "pins": [
-      {
-        "package": "Oxygen",
-        "repositoryURL": "https://github.com/mpangburn/Oxygen.git",
-        "state": {
-          "branch": "master",
-          "revision": "b3c7a6ead1400e4799b16755d23c9905040d4acc",
-          "version": null
-        }
-      }
-    ]
-  },
-  "version": 1
+  "originHash" : "94a024be279d128a7e82f3c76785db1e4cf7c9380d0c4aa59dfdf54952403b8d",
+  "pins" : [
+    {
+      "identity" : "bluecryptor",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueCryptor.git",
+      "state" : {
+        "revision" : "cec97c24b111351e70e448972a7d3fe68a756d6d",
+        "version" : "2.0.2"
+      }
+    },
+    {
+      "identity" : "blueecc",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueECC.git",
+      "state" : {
+        "revision" : "1485268a54f8135435a825a855e733f026fa6cc8",
+        "version" : "1.2.201"
+      }
+    },
+    {
+      "identity" : "bluersa",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueRSA.git",
+      "state" : {
+        "revision" : "f40325520344a966523b214394aa350132a6af68",
+        "version" : "1.0.203"
+      }
+    },
+    {
+      "identity" : "cryptoswift",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
+      "state" : {
+        "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f",
+        "version" : "1.9.0"
+      }
+    },
+    {
+      "identity" : "kituracontracts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/KituraContracts.git",
+      "state" : {
+        "revision" : "6edf7ac3dd2b3a2c61284778d430bbad7d8a6f23",
+        "version" : "2.0.1"
+      }
+    },
+    {
+      "identity" : "loggerapi",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/LoggerAPI.git",
+      "state" : {
+        "revision" : "4e6b45e850ffa275e8e26a24c6454fd709d5b6ac",
+        "version" : "2.0.0"
+      }
+    },
+    {
+      "identity" : "swift-asn1",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-asn1.git",
+      "state" : {
+        "revision" : "810496cf121e525d660cd0ea89a758740476b85f",
+        "version" : "1.5.1"
+      }
+    },
+    {
+      "identity" : "swift-crypto",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-crypto.git",
+      "state" : {
+        "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc",
+        "version" : "3.15.1"
+      }
+    },
+    {
+      "identity" : "swift-jwt",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/Swift-JWT.git",
+      "state" : {
+        "revision" : "f68ec28fbd90a651597e9e825ea7f315f8d52a1f",
+        "version" : "4.0.1"
+      }
+    },
+    {
+      "identity" : "swift-log",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-log.git",
+      "state" : {
+        "revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523",
+        "version" : "1.10.1"
+      }
+    }
+  ],
+  "version" : 3
 }

+ 1 - 79
LoopFollow/Settings/SettingsMenuView.swift

@@ -12,8 +12,6 @@ struct SettingsMenuView: View {
 
     // MARK: – Local state
 
-    @State private var latestVersion: String?
-    @State private var versionTint: Color = .secondary
     @State private var showingTabCustomization = false
 
     // MARK: – Observed objects
@@ -99,7 +97,7 @@ struct SettingsMenuView: View {
                     }
                 }
 
-                // ───────── Advanced / Logs ─────────
+                // ───────── Advanced ─────────
                 Section("Advanced Settings") {
                     NavigationRow(title: "Advanced Settings",
                                   icon: "exclamationmark.shield")
@@ -107,21 +105,6 @@ struct SettingsMenuView: View {
                         settingsPath.value.append(Sheet.advanced)
                     }
                 }
-
-                Section("Logging") {
-                    NavigationRow(title: "View Log",
-                                  icon: "doc.text.magnifyingglass")
-                    {
-                        settingsPath.value.append(Sheet.viewLog)
-                    }
-
-                    ActionRow(title: "Share Logs",
-                              icon: "square.and.arrow.up",
-                              action: shareLogs)
-                }
-
-                // ───────── Build info ─────────
-                buildInfoSection
             }
             .navigationTitle("Settings")
             .navigationDestination(for: Sheet.self) { $0.destination }
@@ -134,7 +117,6 @@ struct SettingsMenuView: View {
                 )
             }
         }
-        .task { await refreshVersionInfo() }
     }
 
     // MARK: – Section builders
@@ -166,64 +148,6 @@ struct SettingsMenuView: View {
         }
     }
 
-    @ViewBuilder
-    private var buildInfoSection: some View {
-        let build = BuildDetails.default
-        let ver = AppVersionManager().version()
-
-        Section("Build Information") {
-            keyValue("Version", ver, tint: versionTint)
-            keyValue("Latest version", latestVersion ?? "Fetching…")
-
-            if !(build.isMacApp() || build.isSimulatorBuild()) {
-                keyValue(build.expirationHeaderString,
-                         dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
-            }
-            keyValue("Built",
-                     dateTimeUtils.formattedDate(from: build.buildDate()))
-            keyValue("Branch", build.branchAndSha)
-        }
-    }
-
-    // MARK: – Helpers
-
-    private func keyValue(_ key: String,
-                          _ value: String,
-                          tint: Color = .secondary) -> some View
-    {
-        HStack {
-            Text(key)
-            Spacer()
-            Text(value).foregroundColor(tint)
-        }
-    }
-
-    private func refreshVersionInfo() async {
-        let mgr = AppVersionManager()
-        let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
-        latestVersion = latest ?? "Unknown"
-
-        let current = mgr.version()
-        versionTint = blacklisted ? .red
-            : newer ? .orange
-            : latest == current ? .green
-            : .secondary
-    }
-
-    private func shareLogs() {
-        let files = LogManager.shared.logFilesForTodayAndYesterday()
-        guard !files.isEmpty else {
-            UIApplication.shared.topMost?.presentSimpleAlert(
-                title: "No Logs Available",
-                message: "There are no logs to share."
-            )
-            return
-        }
-        let avc = UIActivityViewController(activityItems: files,
-                                           applicationActivities: nil)
-        UIApplication.shared.topMost?.present(avc, animated: true)
-    }
-
     private func handleTabReorganization() {
         // Rebuild the tab bar with the new configuration
         MainViewController.rebuildTabsIfNeeded()
@@ -242,7 +166,6 @@ private enum Sheet: Hashable, Identifiable {
     case importExport
     case calendar, contact
     case advanced
-    case viewLog
     case aggregatedStats
 
     var id: Self { self }
@@ -262,7 +185,6 @@ private enum Sheet: Hashable, Identifiable {
         case .calendar: CalendarSettingsView()
         case .contact: ContactSettingsView(viewModel: .init())
         case .advanced: AdvancedSettingsView(viewModel: .init())
-        case .viewLog: LogView(viewModel: .init())
         case .aggregatedStats:
             AggregatedStatsViewWrapper()
         }

+ 214 - 76
LoopFollow/ViewControllers/MoreMenuViewController.swift

@@ -10,21 +10,41 @@ class MoreMenuViewController: UIViewController {
     private var cancellables = Set<AnyCancellable>()
     private var fallbackMainViewController: MainViewController?
 
+    // Build Information state
+    private var latestVersion: String?
+    private var versionTint: UIColor = .secondaryLabel
+
+    // MARK: - Menu models
+
+    enum MenuItemStyle {
+        case navigation
+        case action
+        case detail(String, UIColor)
+        case externalLink
+    }
+
     struct MenuItem {
         let title: String
         let icon: String
-        let subtitle: String?
+        let style: MenuItemStyle
         let action: () -> Void
 
-        init(title: String, icon: String, subtitle: String? = nil, action: @escaping () -> Void) {
+        init(title: String, icon: String, style: MenuItemStyle = .navigation, action: @escaping () -> Void = {}) {
             self.title = title
             self.icon = icon
-            self.subtitle = subtitle
+            self.style = style
             self.action = action
         }
     }
 
-    private var menuSections: [[MenuItem]] = []
+    struct MenuSection {
+        let title: String?
+        let items: [MenuItem]
+    }
+
+    private var menuSections: [MenuSection] = []
+
+    // MARK: - Lifecycle
 
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -53,6 +73,10 @@ class MoreMenuViewController: UIViewController {
 
         setupTableView()
         updateMenuItems()
+
+        Task { [weak self] in
+            await self?.fetchVersionInfo()
+        }
     }
 
     override func viewWillAppear(_ animated: Bool) {
@@ -72,6 +96,8 @@ class MoreMenuViewController: UIViewController {
         }
     }
 
+    // MARK: - Setup
+
     private func setupTableView() {
         tableView = UITableView(frame: view.bounds, style: .insetGrouped)
         tableView.translatesAutoresizingMaskIntoConstraints = false
@@ -89,51 +115,115 @@ class MoreMenuViewController: UIViewController {
         ])
     }
 
+    // MARK: - Menu construction
+
     private func updateMenuItems() {
-        menuSections = []
-
-        // Section 0: Settings (always fixed at top)
-        let settingsSection = [
-            MenuItem(
-                title: "Settings",
-                icon: "gear",
-                action: { [weak self] in
+        let build = BuildDetails.default
+        let ver = AppVersionManager().version()
+
+        menuSections = [
+            // Section 0: Settings
+            MenuSection(title: nil, items: [
+                MenuItem(title: "Settings", icon: "gear") { [weak self] in
                     self?.openSettings()
+                },
+            ]),
+
+            // Section 1: All tab items (static)
+            MenuSection(title: nil, items: TabItem.allCases.map { item in
+                MenuItem(title: item.displayName, icon: item.icon) { [weak self] in
+                    self?.openItem(item)
                 }
-            ),
-        ]
-        menuSections.append(settingsSection)
-
-        let itemsInMenu = Storage.shared.itemsInMenu()
-
-        if !itemsInMenu.isEmpty {
-            var dynamicSection: [MenuItem] = []
-            for item in itemsInMenu {
-                dynamicSection.append(MenuItem(
-                    title: item.displayName,
-                    icon: item.icon,
-                    action: { [weak self] in
-                        self?.openItem(item)
-                    }
+            }),
+
+            // Section 2: Logging
+            MenuSection(title: "Logging", items: [
+                MenuItem(title: "View Log", icon: "doc.text.magnifyingglass") { [weak self] in
+                    self?.openViewLog()
+                },
+                MenuItem(title: "Share Logs", icon: "square.and.arrow.up", style: .action) { [weak self] in
+                    self?.shareLogs()
+                },
+            ]),
+
+            // Section 3: Support & Community
+            MenuSection(title: "Support & Community", items: [
+                MenuItem(title: "LoopFollow Docs", icon: "book", style: .externalLink) { [weak self] in
+                    self?.openURL("https://loopfollowdocs.org/")
+                },
+                MenuItem(title: "Loop and Learn Discord", icon: "bubble.left.and.bubble.right", style: .externalLink) { [weak self] in
+                    self?.openURL("https://discord.gg/TKTuX3dA")
+                },
+                MenuItem(title: "LoopFollow Facebook Group", icon: "person.2.fill", style: .externalLink) { [weak self] in
+                    self?.openURL("https://www.facebook.com/groups/loopfollowlnl")
+                },
+            ]),
+
+            // Section 4: Build Information
+            MenuSection(title: "Build Information", items: {
+                var items: [MenuItem] = [
+                    MenuItem(title: "Version", icon: "", style: .detail(ver, versionTint)),
+                    MenuItem(title: "Latest version", icon: "", style: .detail(latestVersion ?? "Fetching…", .secondaryLabel)),
+                ]
+
+                if !(build.isMacApp() || build.isSimulatorBuild()) {
+                    items.append(MenuItem(
+                        title: build.expirationHeaderString,
+                        icon: "",
+                        style: .detail(dateTimeUtils.formattedDate(from: build.calculateExpirationDate()), .secondaryLabel)
+                    ))
+                }
+
+                items.append(MenuItem(
+                    title: "Built",
+                    icon: "",
+                    style: .detail(dateTimeUtils.formattedDate(from: build.buildDate()), .secondaryLabel)
+                ))
+                items.append(MenuItem(
+                    title: "Branch",
+                    icon: "",
+                    style: .detail(build.branchAndSha, .secondaryLabel)
                 ))
-            }
-            menuSections.append(dynamicSection)
-        }
 
-        // Section: Community
-        let communitySection = [
-            MenuItem(
-                title: "LoopFollow Facebook Group",
-                icon: "person.2.fill",
-                action: { [weak self] in
-                    self?.openFacebookGroup()
-                }
-            ),
+                return items
+            }()),
         ]
-        menuSections.append(communitySection)
     }
 
+    // MARK: - Version fetching
+
+    private func fetchVersionInfo() async {
+        let mgr = AppVersionManager()
+        let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
+        latestVersion = latest ?? "Unknown"
+
+        let current = mgr.version()
+        versionTint = blacklisted ? .systemRed
+            : newer ? .systemOrange
+            : latest == current ? .systemGreen
+            : .secondaryLabel
+
+        await MainActor.run {
+            updateMenuItems()
+            tableView.reloadData()
+        }
+    }
+
+    // MARK: - Navigation
+
     private func openItem(_ item: TabItem) {
+        // If the item is in the tab bar, switch to it
+        if let tabVC = tabBarController,
+           let index = (tabVC.viewControllers ?? []).firstIndex(where: { $0.tabBarItem.title == item.displayName })
+        {
+            tabVC.selectedIndex = index
+            return
+        }
+        // Otherwise present as modal
+        presentItemAsModal(item)
+    }
+
+    private func presentItemAsModal(_ item: TabItem) {
         switch item {
         case .home:
             openHome()
@@ -260,12 +350,10 @@ class MoreMenuViewController: UIViewController {
         let treatmentsVC = UIHostingController(rootView: TreatmentsView())
         let navController = UINavigationController(rootViewController: treatmentsVC)
 
-        // Apply appearance mode
         let style = Storage.shared.appearanceMode.value.userInterfaceStyle
         treatmentsVC.overrideUserInterfaceStyle = style
         navController.overrideUserInterfaceStyle = style
 
-        // Add a close button
         treatmentsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
             image: UIImage(systemName: "checkmark"),
             style: .plain,
@@ -289,12 +377,10 @@ class MoreMenuViewController: UIViewController {
         )
         let navController = UINavigationController(rootViewController: statsVC)
 
-        // Apply appearance mode
         let style = Storage.shared.appearanceMode.value.userInterfaceStyle
         statsVC.overrideUserInterfaceStyle = style
         navController.overrideUserInterfaceStyle = style
 
-        // Add a close button
         statsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
             image: UIImage(systemName: "checkmark"),
             style: .plain,
@@ -308,33 +394,52 @@ class MoreMenuViewController: UIViewController {
     }
 
     private func openHome() {
-        // First check if Home is in the tab bar
-        if let tabVC = tabBarController {
-            for (index, vc) in (tabVC.viewControllers ?? []).enumerated() {
-                if vc is MainViewController {
-                    // Home is in the tab bar, switch to it
-                    tabVC.selectedIndex = index
-                    return
-                }
-            }
-        }
-
-        // Home is in the menu - present the full Home screen as a modal
         let homeModalView = HomeModalView()
         let hostingController = UIHostingController(rootView: homeModalView)
-
         hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
-
         hostingController.modalPresentationStyle = .fullScreen
         present(hostingController, animated: true)
     }
 
-    private func openFacebookGroup() {
-        if let url = URL(string: "https://www.facebook.com/groups/loopfollowlnl") {
+    private func openViewLog() {
+        let logVC = UIHostingController(rootView: LogView())
+        logVC.title = "Log"
+        let navController = UINavigationController(rootViewController: logVC)
+
+        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
+        logVC.overrideUserInterfaceStyle = style
+        navController.overrideUserInterfaceStyle = style
+
+        logVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
+            image: UIImage(systemName: "checkmark"),
+            style: .plain,
+            target: self,
+            action: #selector(dismissModal)
+        )
+        logVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
+
+        navController.modalPresentationStyle = .fullScreen
+        present(navController, animated: true)
+    }
+
+    private func shareLogs() {
+        let files = LogManager.shared.logFilesForTodayAndYesterday()
+        guard !files.isEmpty else {
+            presentSimpleAlert(title: "No Logs Available", message: "There are no logs to share.")
+            return
+        }
+        let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
+        present(avc, animated: true)
+    }
+
+    private func openURL(_ urlString: String) {
+        if let url = URL(string: urlString) {
             UIApplication.shared.open(url)
         }
     }
 
+    // MARK: - Helpers
+
     @objc private func dismissSettingsModal() {
         dismiss(animated: true) {
             // Rebuild tabs after settings is dismissed to apply any tab order changes
@@ -343,7 +448,6 @@ class MoreMenuViewController: UIViewController {
     }
 
     private func getMainViewController() -> MainViewController? {
-        // Try to find MainViewController in the view hierarchy
         guard let tabBarController = tabBarController else { return nil }
 
         for vc in tabBarController.viewControllers ?? [] {
@@ -376,37 +480,71 @@ class MoreMenuViewController: UIViewController {
     }
 }
 
+// MARK: - UITableViewDataSource & UITableViewDelegate
+
 extension MoreMenuViewController: UITableViewDataSource, UITableViewDelegate {
     func numberOfSections(in _: UITableView) -> Int {
         return menuSections.count
     }
 
     func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
-        return menuSections[section].count
+        return menuSections[section].items.count
+    }
+
+    func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
+        return menuSections[section].title
     }
 
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
-        let item = menuSections[indexPath.section][indexPath.row]
-
-        var config = cell.defaultContentConfiguration()
-        config.text = item.title
-        config.image = UIImage(systemName: item.icon)
-
-        if let subtitle = item.subtitle {
-            config.secondaryText = subtitle
-            config.secondaryTextProperties.color = .orange
-            config.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
+        let item = menuSections[indexPath.section].items[indexPath.row]
+
+        switch item.style {
+        case let .detail(value, color):
+            var config = UIListContentConfiguration.valueCell()
+            config.text = item.title
+            config.secondaryText = value
+            config.secondaryTextProperties.color = color
+            cell.contentConfiguration = config
+            cell.accessoryType = .none
+            cell.selectionStyle = .none
+
+        case .externalLink:
+            var config = cell.defaultContentConfiguration()
+            config.text = item.title
+            config.image = UIImage(systemName: item.icon)
+            cell.contentConfiguration = config
+            let linkImage = UIImageView(image: UIImage(systemName: "arrow.up.right.square"))
+            linkImage.tintColor = .tertiaryLabel
+            cell.accessoryView = linkImage
+            cell.selectionStyle = .default
+
+        case .navigation:
+            var config = cell.defaultContentConfiguration()
+            config.text = item.title
+            config.image = UIImage(systemName: item.icon)
+            cell.contentConfiguration = config
+            cell.accessoryView = nil
+            cell.accessoryType = .disclosureIndicator
+            cell.selectionStyle = .default
+
+        case .action:
+            var config = cell.defaultContentConfiguration()
+            config.text = item.title
+            config.image = UIImage(systemName: item.icon)
+            cell.contentConfiguration = config
+            cell.accessoryView = nil
+            cell.accessoryType = .none
+            cell.selectionStyle = .default
         }
 
-        cell.contentConfiguration = config
-        cell.accessoryType = .disclosureIndicator
-
         return cell
     }
 
     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         tableView.deselectRow(at: indexPath, animated: true)
-        menuSections[indexPath.section][indexPath.row].action()
+        let item = menuSections[indexPath.section].items[indexPath.row]
+        if case .detail = item.style { return }
+        item.action()
     }
 }