Ariver
2026-06-13 ed4ebcfa763f66ecd69cae57df2c949a7a9a8a45
src/Apptag/ContentView.swift
@@ -319,8 +319,6 @@
    var bubbleDraftNote = ""
    var tagNavigationHoveredGroupName: String? = nil
    var tagNavigationAppDropTargetName: String? = nil
    var tagNavigationLastHoverScrollID: String? = nil
    var tagNavigationLastHoverScrollAt: Date? = nil
}
private struct EditActionFeedback: Identifiable {
@@ -350,9 +348,13 @@
    @State private var allApps: [AppInfo] = []
    @State private var displayGroups: [TagGroup] = []
    @State private var tagColors: [String: Int] = [:]
    @State private var tagDefinitions: [String: TagDatabase.TagDef] = [:]
    @State private var containerAppOrder: [String: [String]] = [:]
    @State private var groupLayoutVersion = 0
    @State private var appGridScrollTargetID: String? = nil
    @State private var appGridScrollRequestToken = 0
    @State private var selectedUsageTipIndex = 0
    @State private var usageTipsHovered = false
    // Edit mode
    @State private var editPhase: EditPhase = .none
@@ -414,6 +416,7 @@
    @AppStorage("displayMode") private var displayMode = AppDefaults.displayMode
    @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
    @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips
    @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
    @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
    @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
@@ -423,7 +426,8 @@
    private let floatingControlsTrailingInset: CGFloat = 20
    private let floatingControlsReservedWidth: CGFloat = 120
    private var appBubbleDisabled: Bool {
        appGridInteraction.appDragModeActive
        usageTipsHovered
            || appGridInteraction.appDragModeActive
            || appGridInteraction.pendingUncategorizedDrop != nil
            || appGridInteraction.pendingTagRemovalDrop != nil
    }
@@ -432,8 +436,6 @@
            ?? (isColorlessContainerMode ? filledColorlessContainer : nil)
    }
    private let rightSidebarFloatingClearance: CGFloat = 44
    private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
    private var floatingButtonSurfaceColor: Color {
        colorScheme == .dark
            ? Color.white.opacity(0.10)
@@ -450,6 +452,23 @@
    private var isColorlessContainerMode: Bool {
        displayMode == "container" || displayMode == "gridContainer"
    }
    private var shouldShowUsageTips: Bool {
        !hideUsageTips && !allApps.isEmpty && !quickSearchOnlySession
    }
    private var appGridUsageTips: [AppGridUsageTip] {
        [
            AppGridUsageTip(id: 1, titleKey: "usageTips.tip1.title", detailKey: "usageTips.tip1.detail"),
            AppGridUsageTip(id: 2, titleKey: "usageTips.tip2.title", detailKey: "usageTips.tip2.detail"),
            AppGridUsageTip(id: 3, titleKey: "usageTips.tip3.title", detailKey: "usageTips.tip3.detail"),
            AppGridUsageTip(id: 4, titleKey: "usageTips.tip4.title", detailKey: "usageTips.tip4.detail"),
            AppGridUsageTip(id: 5, titleKey: "usageTips.tip5.title", detailKey: "usageTips.tip5.detail"),
            AppGridUsageTip(id: 6, titleKey: "usageTips.tip6.title", detailKey: "usageTips.tip6.detail"),
            AppGridUsageTip(id: 7, titleKey: "usageTips.tip7.title", detailKey: "usageTips.tip7.detail"),
            AppGridUsageTip(id: 8, titleKey: "usageTips.tip8.title", detailKey: "usageTips.tip8.detail"),
        ]
    }
    private var shouldRenderAppGridBehindQuickSearch: Bool {
@@ -623,6 +642,9 @@
        .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
            publishModalInteractionState()
        }
        .onChange(of: smartStartNotice != nil) { _, _ in
            publishModalInteractionState()
        }
        .onDisappear {
            NotificationCenter.default.post(
                name: .tagLauncherModalInteractionChanged,
@@ -636,7 +658,11 @@
        NotificationCenter.default.post(
            name: .tagLauncherModalInteractionChanged,
            object: nil,
            userInfo: ["active": appGridInteraction.pendingUncategorizedDrop != nil || appGridInteraction.pendingTagRemovalDrop != nil]
            userInfo: [
                "active": smartStartNotice != nil
                    || appGridInteraction.pendingUncategorizedDrop != nil
                    || appGridInteraction.pendingTagRemovalDrop != nil
            ]
        )
    }
@@ -1116,6 +1142,10 @@
                    bubbleDisabled: appBubbleDisabled,
                    showUncommonAppBubbles: showUncommonAppBubbles,
                    highlightedGroupName: appGridHighlightedGroupName,
                    bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsMetrics.reservedHeight : 0,
                    usageTipsVisible: shouldShowUsageTips,
                    usageTips: appGridUsageTips,
                    selectedUsageTipIndex: $selectedUsageTipIndex,
                    contentRevision: groupLayoutVersion,
                    scrollTargetID: appGridScrollTargetID,
                    scrollRequestToken: appGridScrollRequestToken,
@@ -1128,13 +1158,17 @@
                    onDropOutsideGroup: { path, source, copy in
                        dropAppOutsideGroup(path: path, sourceTag: source, copy: copy)
                    },
                    onReorderApps: { containerID, orderedPaths in
                        reorderApps(inContainer: containerID, orderedPaths: orderedPaths)
                    },
                    onGroupActivate: { groupName in
                        if isColorlessContainerMode {
                            toggleColorlessFill(groupName)
                        }
                    },
                    onScrollActivity: handleAppGridScrollActivity,
                    onDragModeChange: { setAppDragMode($0) }
                    onDragModeChange: { setAppDragMode($0) },
                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                )
            }
        }
@@ -1775,13 +1809,27 @@
    private func makeDisplayGroups(apps: [AppInfo], tagOrder: [String]) -> [TagGroup] {
        let order = tagOrder.isEmpty ? TagEditor.orderedTagNames() : tagOrder
        let rawGroups = AppIndexer.group(apps: apps, defaultGroupName: defaultGroupName, tagOrder: order)
        let rawGroups = AppIndexer.group(
            apps: apps,
            defaultGroupName: defaultGroupName,
            tagOrder: order,
            tagDefinitions: tagDefinitions,
            containerAppOrder: containerAppOrder
        )
        return rawGroups.map { group in
            if group.name == defaultGroupName {
                return TagGroup(name: tr("group.uncategorized"), apps: group.apps)
                return TagGroup(
                    containerID: group.containerID,
                    name: tr("group.uncategorized"),
                    apps: group.apps
                )
            }
            if group.name == "Mac自带" {
                return TagGroup(name: tr("group.appleBuiltIn"), apps: group.apps)
                return TagGroup(
                    containerID: group.containerID,
                    name: tr("group.appleBuiltIn"),
                    apps: group.apps
                )
            }
            return group
        }
@@ -1840,6 +1888,13 @@
    private func handleAppGridScrollActivity() {
        if appGridInteraction.hoveredBubble != nil {
            appGridInteraction.hoveredBubble = nil
        }
    }
    private func handleUsageTipsHoverChange(_ hovering: Bool) {
        usageTipsHovered = hovering
        if hovering, appGridInteraction.hoveredBubble != nil {
            appGridInteraction.hoveredBubble = nil
        }
    }
@@ -2001,7 +2056,6 @@
        appGridInteraction.tagNavigationHoveredGroupName = id
        fillColorlessContainer(id)
        scrollToTagFromHover(id)
    }
    private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2020,18 +2074,6 @@
        }
        appGridInteraction.tagNavigationAppDropTargetName = id
    }
    private func scrollToTagFromHover(_ id: String) {
        let now = Date()
        if appGridInteraction.tagNavigationLastHoverScrollID == id,
           let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
           now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
            return
        }
        appGridInteraction.tagNavigationLastHoverScrollID = id
        appGridInteraction.tagNavigationLastHoverScrollAt = now
        scrollTo(id)
    }
    private func fillColorlessContainer(_ id: String) {
@@ -2136,6 +2178,8 @@
    private func dropApp(path: String, sourceTag: String, targetTag: String, copy: Bool) {
        resetTransientDragState(keepingPendingUncategorizedDrop: true)
        guard sourceTag != targetTag else { return }
        if isUncategorizedDropTarget(targetTag) {
            confirmAndMoveAppToUncategorized(path: path)
            return
@@ -2147,7 +2191,6 @@
        }
        guard tagColors[targetTag] != nil else { return }
        guard sourceTag != targetTag || copy else { return }
        TagEditor.moveApp(
            path: path,
            from: sourceTag,
@@ -2166,6 +2209,18 @@
        guard !appHasTag(app, tagName: targetTag) else { return }
        TagEditor.appendTags([targetTag], to: [path])
        showDropRefresh()
        refreshApps(forceLayoutRefresh: true)
    }
    private func reorderApps(inContainer containerID: String, orderedPaths: [String]) {
        resetTransientDragState()
        let key = TagDatabase.normalizedContainerID(containerID, tags: tagDefinitions)
        let paths = TagDatabase.normalizedAppOrderPaths(orderedPaths)
        guard !key.isEmpty, !paths.isEmpty else { return }
        containerAppOrder[key] = paths
        rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
        TagEditor.reorderApps(inContainer: key, orderedPaths: paths)
        refreshApps(forceLayoutRefresh: true)
    }
@@ -2472,6 +2527,8 @@
        allApps = snapshot.apps
        quickSearchDocuments = snapshot.quickSearchDocuments
        tagColors = snapshot.tagColors
        tagDefinitions = snapshot.tagDefinitions
        containerAppOrder = snapshot.containerAppOrder
        draggedTagNames = snapshot.tagOrder
        rebuildDisplayGroups(apps: snapshot.apps, tagOrder: snapshot.tagOrder)
        if quickSearchVisible {