From fb4eaa3834c5c62c5421dc67c3cdb546b1a5a0f9 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 05:46:37 +0800
Subject: [PATCH] Modularize settings smart start paths

---
 Apptag/ContentView.swift |  186 +++++++++++-----------------------------------
 1 files changed, 44 insertions(+), 142 deletions(-)

diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index da98d57..e5389cd 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -340,20 +340,6 @@
     let tagName: String
 }
 
-private enum SmartStartNoticeMode {
-    case autoApplied
-    case suggestionOnly
-    case manuallyApplied
-}
-
-private struct SmartStartNotice: Identifiable {
-    let id = UUID()
-    let mode: SmartStartNoticeMode
-    let title: String
-    let message: String
-    let summary: SmartStartSummary
-}
-
 struct ContentView: View {
     let hideOverlay: () -> Void
     private let initialQuickSearchSource: String?
@@ -664,7 +650,7 @@
                     }
                     .zIndex(899)
 
-                QuickSearchOverlayView(
+                QuickSearchPanelPresentationView(
                     query: $quickSearchQuery,
                     results: quickSearchResults,
                     selectedID: quickSearchSelectedID,
@@ -672,16 +658,15 @@
                     selectionScrollToken: quickSearchSelectionScrollToken,
                     isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
                     maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
+                    panelTopY: quickSearchPanelTopY(in: proxy.size),
+                    panelHeight: quickSearchPanelContentHeight(in: proxy.size),
                     errorMessage: quickSearchErrorMessage,
                     onCommand: handleQuickSearchCommand,
                     onHover: selectQuickSearchResult,
                     onLaunch: launchQuickSearchResult
                 )
-                .position(
-                    x: proxy.size.width / 2,
-                    y: quickSearchPanelCenterY(in: proxy.size)
-                )
-                .transition(.scale(scale: 0.98).combined(with: .opacity))
+                .frame(width: 0, height: 0)
+                .allowsHitTesting(false)
                 .zIndex(900)
             }
         }
@@ -689,10 +674,15 @@
         .animation(.easeOut(duration: 0.12), value: quickSearchVisible)
     }
 
-    private func quickSearchPanelCenterY(in size: CGSize) -> CGFloat {
-        let visibleRows = max(1, min(quickSearchResults.isEmpty ? 1 : quickSearchResults.count, quickSearchMaxVisibleRows(in: size)))
-        let estimatedPanelHeight = CGFloat(visibleRows) * 76 + 122
-        return quickSearchPanelTopY(in: size) + estimatedPanelHeight / 2
+    private func quickSearchPanelContentHeight(in size: CGSize) -> CGFloat {
+        let hasResultList = !(quickSearchDocuments.isEmpty && refreshInProgress)
+            && quickSearchErrorMessage == nil
+            && !quickSearchResults.isEmpty
+        let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
+        return QuickSearchPanelMetrics.contentHeight(
+            hasResultList: hasResultList,
+            visibleRows: visibleRows
+        )
     }
 
     private func quickSearchPanelTopY(in size: CGSize) -> CGFloat {
@@ -701,8 +691,11 @@
 
     private func quickSearchMaxVisibleRows(in size: CGSize) -> Int {
         let bottomClearance: CGFloat = 84
-        let chromeHeight: CGFloat = 122
-        let rowHeightWithSpacing: CGFloat = 76
+        let chromeHeight = QuickSearchPanelMetrics.headerHeight
+            + QuickSearchPanelMetrics.dividerHeight
+            + QuickSearchPanelMetrics.resultListVerticalInset * 2
+        let rowHeightWithSpacing = QuickSearchPanelMetrics.rowHeight
+            + QuickSearchPanelMetrics.rowSpacing
         let availableHeight = max(0, size.height - quickSearchPanelTopY(in: size) - bottomClearance - chromeHeight)
         return max(1, min(8, Int(floor(availableHeight / rowHeightWithSpacing))))
     }
@@ -1093,88 +1086,12 @@
     }
 
     private var smartStartNoticeOverlay: some View {
-        GeometryReader { proxy in
-            if let notice = smartStartNotice {
-                let panelWidth = min(560, max(320, proxy.size.width - 64))
-
-                ZStack {
-                    Color.black.opacity(0.10)
-                        .ignoresSafeArea()
-
-                    VStack(spacing: 18) {
-                        Image(systemName: notice.mode == .suggestionOnly ? "sparkles" : "checkmark.seal.fill")
-                            .font(.system(size: 30, weight: .semibold))
-                            .foregroundStyle(Color.accentColor)
-                            .frame(width: 58, height: 58)
-                            .background(
-                                Circle()
-                                    .fill(Color.accentColor.opacity(0.12))
-                            )
-
-                        VStack(spacing: 8) {
-                            Text(notice.title)
-                                .font(.system(size: 21, weight: .semibold))
-                                .foregroundStyle(.primary)
-                                .multilineTextAlignment(.center)
-
-                            Text(notice.message)
-                                .font(.system(size: 15, weight: .regular))
-                                .foregroundStyle(.secondary)
-                                .multilineTextAlignment(.center)
-                                .lineLimit(4)
-                                .fixedSize(horizontal: false, vertical: true)
-                        }
-
-                        HStack(spacing: 12) {
-                            switch notice.mode {
-                            case .suggestionOnly:
-                                Button(tr("smartstart.later")) {
-                                    dismissSmartStartNotice()
-                                }
-                                .buttonStyle(.bordered)
-                                .controlSize(.large)
-
-                                Button(tr("smartstart.apply")) {
-                                    applySmartStartSuggestion()
-                                }
-                                .buttonStyle(.borderedProminent)
-                                .controlSize(.large)
-                            case .autoApplied, .manuallyApplied:
-                                if notice.summary.backupPath != nil {
-                                    Button(tr("smartstart.undo")) {
-                                        undoSmartStart()
-                                    }
-                                    .buttonStyle(.bordered)
-                                    .controlSize(.large)
-                                }
-                                Button(tr("smartstart.ok")) {
-                                    dismissSmartStartNotice()
-                                }
-                                .buttonStyle(.borderedProminent)
-                                .controlSize(.large)
-                            }
-                        }
-                    }
-                    .padding(.horizontal, 30)
-                    .padding(.vertical, 26)
-                    .frame(width: panelWidth)
-                    .background(
-                        RoundedRectangle(cornerRadius: 16)
-                            .fill(.ultraThickMaterial)
-                            .shadow(color: .black.opacity(0.26), radius: 28, y: 16)
-                    )
-                    .overlay(
-                        RoundedRectangle(cornerRadius: 16)
-                            .stroke(.white.opacity(0.16), lineWidth: 1)
-                    )
-                }
-                .frame(width: proxy.size.width, height: proxy.size.height)
-                .transition(.scale(scale: 0.96).combined(with: .opacity))
-                .zIndex(650)
-            }
-        }
-        .ignoresSafeArea()
-        .allowsHitTesting(smartStartNotice != nil)
+        SmartStartNoticeOverlay(
+            notice: smartStartNotice,
+            onDismiss: dismissSmartStartNotice,
+            onApply: applySmartStartSuggestion,
+            onUndo: undoSmartStart
+        )
     }
     // MARK: - Edit Tags View
 
@@ -1695,23 +1612,14 @@
 
         let scannedApps = allApps
         DispatchQueue.global(qos: .userInitiated).async {
-            let result = SmartStartService.applySuggestion(draft)
-            let store = result.store
-            let apps = TagEditor.annotate(apps: scannedApps, store: store)
-            let quickSearchDocs = QuickSearchEngine.makeDocuments(apps: apps, store: store)
-            let colors = store.tags.mapValues { $0.color }
-            let order = TagEditor.orderedTagNames()
+            let result = AppLibraryController.applySmartStartSuggestion(
+                draft,
+                scannedApps: scannedApps
+            )
 
             DispatchQueue.main.async {
                 pendingSmartStartDraft = nil
-                allApps = apps
-                quickSearchDocuments = quickSearchDocs
-                tagColors = colors
-                draggedTagNames = order
-                rebuildDisplayGroups(apps: apps, tagOrder: order)
-                if quickSearchVisible {
-                    refreshQuickSearchResults()
-                }
+                applyAppLibrarySnapshot(result.snapshot)
                 if let summary = result.summary {
                     showSmartStartNotice(mode: .manuallyApplied, summary: summary)
                 } else {
@@ -2363,27 +2271,10 @@
 
         refreshInProgress = true
         DispatchQueue.global(qos: .userInitiated).async {
-            let scannedApps = AppIndexer.scan()
-            let reconciledStore = TagEditor.reconcileScannedApps(scannedApps)
-            let smartStartResult = SmartStartService.runIfNeeded(
-                apps: scannedApps,
-                store: reconciledStore
-            )
-            let store = smartStartResult.store
-            let apps = TagEditor.annotate(apps: scannedApps, store: store)
-            let quickSearchDocs = QuickSearchEngine.makeDocuments(apps: apps, store: store)
-            let colors = store.tags.mapValues { $0.color }
-            let order = TagEditor.orderedTagNames()
+            let result = AppLibraryController.refresh()
             DispatchQueue.main.async {
-                allApps = apps
-                quickSearchDocuments = quickSearchDocs
-                tagColors = colors
-                draggedTagNames = order
-                rebuildDisplayGroups(apps: apps, tagOrder: order)
-                if quickSearchVisible {
-                    refreshQuickSearchResults()
-                }
-                handleSmartStartRunResult(smartStartResult)
+                applyAppLibrarySnapshot(result.snapshot)
+                handleSmartStartRunResult(result.smartStartResult)
                 if forceLayoutRefresh {
                     finishDropRefreshAfterMinimumDuration()
                 }
@@ -2398,6 +2289,17 @@
         }
     }
 
+    private func applyAppLibrarySnapshot(_ snapshot: AppLibrarySnapshot) {
+        allApps = snapshot.apps
+        quickSearchDocuments = snapshot.quickSearchDocuments
+        tagColors = snapshot.tagColors
+        draggedTagNames = snapshot.tagOrder
+        rebuildDisplayGroups(apps: snapshot.apps, tagOrder: snapshot.tagOrder)
+        if quickSearchVisible {
+            refreshQuickSearchResults()
+        }
+    }
+
     private func launchApp(
         _ app: AppInfo,
         closeQuickSearchOnSuccess: Bool = false,

--
Gitblit v1.9.3