@@ -142,6 +142,25 @@ final class AppStore: ObservableObject {
142142 }
143143 }
144144
145+ enum DevelopmentBackgroundOverride : String , CaseIterable , Identifiable {
146+ case none
147+ case solidWhite
148+ case solidBlack
149+
150+ var id : String { rawValue }
151+
152+ var color : Color ? {
153+ switch self {
154+ case . none:
155+ return nil
156+ case . solidWhite:
157+ return . white
158+ case . solidBlack:
159+ return . black
160+ }
161+ }
162+ }
163+
145164 enum SidebarIconPreset : String , CaseIterable , Identifiable {
146165 case large
147166 case medium
@@ -203,6 +222,7 @@ final class AppStore: ObservableObject {
203222 static let activePressEffectKey = " enableActivePressEffect "
204223 static let activePressScaleKey = " activePressScale "
205224 static let followScrollPagingKey = " followScrollPagingEnabled "
225+ static let reverseWheelPagingKey = " reverseWheelPagingDirection "
206226 static let useCAGridRendererKey = " useCAGridRenderer "
207227 static let backgroundStyleKey = " launchpadBackgroundStyle "
208228 static let backgroundMaskEnabledKey = " launchpadBackgroundMaskEnabled "
@@ -222,6 +242,8 @@ final class AppStore: ObservableObject {
222242 private static let voiceFeedbackEnabledKey = " voiceFeedbackEnabled "
223243 static let folderDropZoneScaleKey = " folderDropZoneScale "
224244 static let pageIndicatorTopPaddingKey = " pageIndicatorTopPadding "
245+ static let onboardingVersionKey = " onboardingVersionShown "
246+ static let currentOnboardingVersion = 1
225247 // private static let aiFeatureEnabledKey = "aiFeatureEnabled"
226248 // private static let aiOverlayHotKeyKey = "aiOverlayHotKeyConfiguration"
227249
@@ -437,6 +459,9 @@ final class AppStore: ObservableObject {
437459 }
438460 }
439461
462+ // Development-only override to capture flat screenshots quickly.
463+ @Published var developmentBackgroundOverride : DevelopmentBackgroundOverride = . none
464+
440465 @Published var backgroundMaskEnabled : Bool = AppStore . loadBackgroundMaskEnabled ( ) {
441466 didSet {
442467 UserDefaults . standard. set ( backgroundMaskEnabled, forKey: Self . backgroundMaskEnabledKey)
@@ -493,7 +518,26 @@ final class AppStore: ObservableObject {
493518 useLocalizedThirdPartyTitles = UserDefaults . standard. object ( forKey: " useLocalizedThirdPartyTitles " ) as? Bool ?? true
494519 enableAnimations = UserDefaults . standard. object ( forKey: " enableAnimations " ) as? Bool ?? true
495520 scrollSensitivity = UserDefaults . standard. object ( forKey: " scrollSensitivity " ) as? Double ?? scrollSensitivity
496- useCAGridRenderer = UserDefaults . standard. object ( forKey: Self . useCAGridRendererKey) as? Bool ?? false
521+ reverseWheelPagingDirection = UserDefaults . standard. object ( forKey: Self . reverseWheelPagingKey) as? Bool ?? false
522+ useCAGridRenderer = UserDefaults . standard. object ( forKey: Self . useCAGridRendererKey) as? Bool ?? useCAGridRenderer
523+
524+ // Keep imported appearance/input settings in sync without requiring relaunch.
525+ iconScale = UserDefaults . standard. object ( forKey: " iconScale " ) as? Double ?? iconScale
526+ iconLabelFontSize = UserDefaults . standard. object ( forKey: " iconLabelFontSize " ) as? Double ?? iconLabelFontSize
527+ if let rawFontWeight = UserDefaults . standard. string ( forKey: Self . iconLabelFontWeightKey) ,
528+ let fontWeight = IconLabelFontWeightOption ( rawValue: rawFontWeight) {
529+ iconLabelFontWeight = fontWeight
530+ }
531+
532+ pageIndicatorOffset = UserDefaults . standard. object ( forKey: " pageIndicatorOffset " ) as? Double ?? pageIndicatorOffset
533+ let importedTopPadding = UserDefaults . standard. object ( forKey: Self . pageIndicatorTopPaddingKey) as? Double ?? pageIndicatorTopPadding
534+ pageIndicatorTopPadding = Self . clampPageIndicatorTopPadding ( importedTopPadding)
535+ if let importedPerDisplayEnabled = UserDefaults . standard. object ( forKey: Self . pageIndicatorPerDisplayEnabledKey) as? Bool {
536+ pageIndicatorPerDisplayEnabled = importedPerDisplayEnabled
537+ }
538+ pageIndicatorOverrides = Self . loadPageIndicatorOverrides ( )
539+
540+ globalHotKey = Self . loadHotKeyConfiguration ( )
497541
498542 // Apply hidden filtering immediately
499543 pruneHiddenAppsFromAppList ( )
@@ -505,6 +549,7 @@ final class AppStore: ObservableObject {
505549 }
506550 @Published var isSetting = false
507551 @Published var isInitialLoading = true
552+ @Published var shouldShowOnboarding : Bool = false
508553 @Published var currentPage = 0 {
509554 didSet {
510555 if currentPage < 0 { currentPage = 0 ; return }
@@ -788,6 +833,13 @@ final class AppStore: ObservableObject {
788833 didSet { UserDefaults . standard. set ( followScrollPagingEnabled, forKey: Self . followScrollPagingKey) }
789834 }
790835
836+ @Published var reverseWheelPagingDirection : Bool = {
837+ if UserDefaults . standard. object ( forKey: AppStore . reverseWheelPagingKey) == nil { return false }
838+ return UserDefaults . standard. bool ( forKey: AppStore . reverseWheelPagingKey)
839+ } ( ) {
840+ didSet { UserDefaults . standard. set ( reverseWheelPagingDirection, forKey: Self . reverseWheelPagingKey) }
841+ }
842+
791843 @Published var useCAGridRenderer : Bool = {
792844 if UserDefaults . standard. object ( forKey: AppStore . useCAGridRendererKey) == nil { return true }
793845 let enabled = UserDefaults . standard. bool ( forKey: AppStore . useCAGridRendererKey)
@@ -1562,7 +1614,7 @@ final class AppStore: ObservableObject {
15621614 self . isFullscreenMode = UserDefaults . standard. bool ( forKey: " isFullscreenMode " )
15631615 }
15641616 if UserDefaults . standard. object ( forKey: PerformanceMode . userDefaultsKey) == nil {
1565- PerformanceMode . persist ( . full )
1617+ PerformanceMode . persist ( . lean )
15661618 }
15671619 let defaults = UserDefaults . standard
15681620
@@ -1629,6 +1681,9 @@ final class AppStore: ObservableObject {
16291681 if UserDefaults . standard. object ( forKey: AppStore . followScrollPagingKey) == nil {
16301682 UserDefaults . standard. set ( false , forKey: AppStore . followScrollPagingKey)
16311683 }
1684+ if UserDefaults . standard. object ( forKey: AppStore . reverseWheelPagingKey) == nil {
1685+ UserDefaults . standard. set ( false , forKey: AppStore . reverseWheelPagingKey)
1686+ }
16321687 if defaults. object ( forKey: Self . gameControllerMenuToggleKey) == nil {
16331688 defaults. set ( true , forKey: Self . gameControllerMenuToggleKey)
16341689 }
@@ -1783,6 +1838,7 @@ final class AppStore: ObservableObject {
17831838
17841839 func configure( modelContext: ModelContext ) {
17851840 self . modelContext = modelContext
1841+ evaluateOnboardingGate ( )
17861842
17871843 // 立即尝试加载持久化数据(如果已有数据)——不要过早设置标记,等待加载完成时设置
17881844 if !hasAppliedOrderFromStore {
@@ -1814,13 +1870,62 @@ final class AppStore: ObservableObject {
18141870 . store ( in: & cancellables)
18151871 }
18161872
1873+ func completeOnboarding( ) {
1874+ UserDefaults . standard. set ( Self . currentOnboardingVersion, forKey: Self . onboardingVersionKey)
1875+ shouldShowOnboarding = false
1876+ }
1877+
1878+ func forceShowOnboarding( ) {
1879+ guard isFullscreenMode else { return }
1880+
1881+ if isSetting {
1882+ isSetting = false
1883+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.12 ) { [ weak self] in
1884+ guard let self else { return }
1885+ self . shouldShowOnboarding = false
1886+ self . shouldShowOnboarding = true
1887+ }
1888+ return
1889+ }
1890+
1891+ shouldShowOnboarding = false
1892+ DispatchQueue . main. async { [ weak self] in
1893+ self ? . shouldShowOnboarding = true
1894+ }
1895+ }
1896+
1897+ private func evaluateOnboardingGate( ) {
1898+ let shownVersion = UserDefaults . standard. object ( forKey: Self . onboardingVersionKey) as? Int ?? 0
1899+ guard shownVersion < Self . currentOnboardingVersion else {
1900+ shouldShowOnboarding = false
1901+ return
1902+ }
1903+
1904+ if isExistingUserForOnboarding ( ) {
1905+ UserDefaults . standard. set ( Self . currentOnboardingVersion, forKey: Self . onboardingVersionKey)
1906+ shouldShowOnboarding = false
1907+ return
1908+ }
1909+
1910+ shouldShowOnboarding = true
1911+ }
1912+
1913+ private func isExistingUserForOnboarding( ) -> Bool {
1914+ if !hiddenAppPaths. isEmpty { return true }
1915+ if !customTitles. isEmpty { return true }
1916+ if hasPersistedOrderData ( ) { return true }
1917+ return false
1918+ }
1919+
18171920 // MARK: - Order Persistence
18181921 func applyOrderAndFolders( ) {
18191922 self . loadAllOrder ( )
18201923 }
18211924
18221925 // MARK: - Initial scan (once)
18231926 func performInitialScanIfNeeded( ) {
1927+ guard !hasPerformedInitialScan else { return }
1928+
18241929 // 先尝试加载持久化数据,避免被扫描覆盖(不提前设置标记)
18251930 if !hasAppliedOrderFromStore {
18261931 loadAllOrder ( )
@@ -2994,13 +3099,18 @@ final class AppStore: ObservableObject {
29943099 /// 单页内自动补位:将每页的 .empty 槽位移动到该页尾部,保持非空项的相对顺序
29953100 func compactItemsWithinPages( ) {
29963101 guard !items. isEmpty else { return }
3102+ items = filteredItemsRemovingHidden ( from: compactedItemsWithinPages ( items) )
3103+ }
3104+
3105+ private func compactedItemsWithinPages( _ source: [ LaunchpadItem ] ) -> [ LaunchpadItem ] {
3106+ guard !source. isEmpty else { return source }
29973107 let itemsPerPage = self . itemsPerPage // 使用计算属性
29983108 var result : [ LaunchpadItem ] = [ ]
2999- result. reserveCapacity ( items . count)
3109+ result. reserveCapacity ( source . count)
30003110 var index = 0
3001- while index < items . count {
3002- let end = min ( index + itemsPerPage, items . count)
3003- let pageSlice = Array ( items [ index..< end] )
3111+ while index < source . count {
3112+ let end = min ( index + itemsPerPage, source . count)
3113+ let pageSlice = Array ( source [ index..< end] )
30043114 var nonEmpty : [ LaunchpadItem ] = [ ]
30053115 var emptyTokens : [ String ] = [ ]
30063116 nonEmpty. reserveCapacity ( pageSlice. count)
@@ -3025,10 +3135,60 @@ final class AppStore: ObservableObject {
30253135
30263136 index = end
30273137 }
3028- items = filteredItemsRemovingHidden ( from : result)
3138+ return result
30293139 }
30303140
30313141 // MARK: - 跨页拖拽:级联插入(满页则将最后一个推入下一页)
3142+ func moveSelectedAppsAcrossPagesWithCascade( appPathsOrdered: [ String ] , to targetIndex: Int ) {
3143+ guard !appPathsOrdered. isEmpty else { return }
3144+
3145+ var seenPaths = Set < String > ( )
3146+ let normalizedOrderedPaths : [ String ] = appPathsOrdered. compactMap { raw in
3147+ let normalized = standardizedFilePath ( raw)
3148+ guard seenPaths. insert ( normalized) . inserted else { return nil }
3149+ return normalized
3150+ }
3151+ guard !normalizedOrderedPaths. isEmpty else { return }
3152+ let movingPathSet = Set ( normalizedOrderedPaths)
3153+
3154+ var movingItemsByPath : [ String : LaunchpadItem ] = [ : ]
3155+ for item in items {
3156+ guard case . app( let app) = item else { continue }
3157+ let path = standardizedFilePath ( app. url. path)
3158+ if movingPathSet. contains ( path) , movingItemsByPath [ path] == nil {
3159+ movingItemsByPath [ path] = . app( app)
3160+ }
3161+ }
3162+
3163+ let orderedMovingItems = normalizedOrderedPaths. compactMap { movingItemsByPath [ $0] }
3164+ guard !orderedMovingItems. isEmpty else { return }
3165+
3166+ var result = items
3167+ let sourceIndexes = result. indices. filter { index in
3168+ guard case . app( let app) = result [ index] else { return false }
3169+ return movingPathSet. contains ( standardizedFilePath ( app. url. path) )
3170+ }
3171+ guard !sourceIndexes. isEmpty else { return }
3172+
3173+ for index in sourceIndexes {
3174+ result [ index] = . empty( UUID ( ) . uuidString)
3175+ }
3176+
3177+ result = compactedItemsWithinPages ( result)
3178+ var insertionIndex = max ( 0 , min ( targetIndex, result. count) )
3179+
3180+ for movingItem in orderedMovingItems {
3181+ result = cascadeInsert ( into: result, item: movingItem, at: insertionIndex)
3182+ insertionIndex += 1
3183+ }
3184+
3185+ items = filteredItemsRemovingHidden ( from: result)
3186+ compactItemsWithinPages ( )
3187+ removeEmptyPages ( )
3188+ triggerGridRefresh ( )
3189+ saveAllOrder ( )
3190+ }
3191+
30323192 func moveItemAcrossPagesWithCascade( item: LaunchpadItem , to targetIndex: Int ) {
30333193 guard items. indices. contains ( targetIndex) || targetIndex == items. count else {
30343194 return
0 commit comments