Ariver
2026-05-13 2a028d08370a86aeca124efc9a7b3e52ac32c367
Apptag/ContentView.swift
@@ -110,7 +110,10 @@
struct TagPill: View {
    let name: String
    let colorIndex: Int
    var dragModeActive: Bool = false
    var isDragging: Bool = false
    let action: () -> Void
    @State private var wiggle = false
    private var bgColor: Color {
        Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -124,16 +127,28 @@
    }
    var body: some View {
        Button(action: action) {
            Text(name)
                .font(.system(size: 13, weight: .medium))
                .foregroundStyle(textColor)
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .background(RoundedRectangle(cornerRadius: 7).fill(bgColor))
                .shadow(color: .black.opacity(0.2), radius: 3, y: 1)
        Text(name)
            .font(.system(size: 13, weight: .medium))
            .foregroundStyle(textColor)
            .padding(.horizontal, 12)
            .padding(.vertical, 6)
            .background(RoundedRectangle(cornerRadius: 7).fill(bgColor))
            .shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
            .scaleEffect(isDragging ? 1.05 : 1.0)
            .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.8 : -1.8) : 0))
            .animation(
                dragModeActive
                    ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
                    : .default,
                value: wiggle
            )
            .onChange(of: dragModeActive) { _, active in
                wiggle = active
            }
            .contentShape(RoundedRectangle(cornerRadius: 7))
            .onTapGesture {
                if !dragModeActive { action() }
        }
        .buttonStyle(.plain)
    }
}
@@ -142,7 +157,10 @@
struct SideTagPill: View {
    let name: String
    let colorIndex: Int
    var dragModeActive: Bool = false
    var isDragging: Bool = false
    let action: () -> Void
    @State private var wiggle = false
    private var bgColor: Color {
        Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -152,16 +170,28 @@
    }
    var body: some View {
        Button(action: action) {
            Text(name)
                .font(.system(size: 13, weight: .medium))
                .foregroundStyle(textColor)
                .padding(.horizontal, 10).padding(.vertical, 5)
                .frame(maxWidth: .infinity, alignment: .leading)
                .background(RoundedRectangle(cornerRadius: 6).fill(bgColor))
                .shadow(color: .black.opacity(0.2), radius: 3, y: 1)
        Text(name)
            .font(.system(size: 13, weight: .medium))
            .foregroundStyle(textColor)
            .padding(.horizontal, 10).padding(.vertical, 5)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(RoundedRectangle(cornerRadius: 6).fill(bgColor))
            .shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
            .scaleEffect(isDragging ? 1.03 : 1.0)
            .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.6 : -1.6) : 0))
            .animation(
                dragModeActive
                    ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
                    : .default,
                value: wiggle
            )
            .onChange(of: dragModeActive) { _, active in
                wiggle = active
            }
            .contentShape(RoundedRectangle(cornerRadius: 6))
            .onTapGesture {
                if !dragModeActive { action() }
        }
        .buttonStyle(.plain)
    }
}
@@ -204,6 +234,10 @@
    @State private var successToast: String? = nil
    @State private var draggedTagNames: [String] = []  // live drag order
    @State private var dragItem: String? = nil          // currently dragged tag
    @State private var tagReorderFrames: [String: CGRect] = [:]
    @State private var tagNavDragModeActive = false
    @State private var tagNavDragItem: String? = nil
    @State private var tagNavReorderFrames: [String: CGRect] = [:]
    @State private var hoveredContainer: String? = nil  // colored container lift
    // Fixed interaction for "Colorless Container": hover fills persistently; click clears.
    @State private var filledColorlessContainer: String? = nil
@@ -387,12 +421,17 @@
            HStack(spacing: 8) {
                ForEach(tagLabels) { tag in
                    TagPill(name: tag.name, colorIndex: tag.colorIndex,
                            dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
                            isDragging: tagNavDragItem == tag.name,
                            action: {
                        if isColorlessContainerMode {
                            toggleColorlessFill(tag.id)
                        }
                        scrollTo(tag.id)
                    })
                    .background(tagNavFrameReader(for: tag.name))
                    .zIndex(tagNavDragItem == tag.name ? 1 : 0)
                    .highPriorityGesture(tagNavReorderGesture(for: tag.name))
                    .onHover { hovering in
                        if hovering {
                            fillColorlessContainer(tag.id)
@@ -400,7 +439,12 @@
                        }
                    }
                }
            }.padding(.horizontal, 24)
            }
            .padding(.horizontal, 24)
            .coordinateSpace(name: "tagNavReorder")
            .onPreferenceChange(TagNavReorderFramePreferenceKey.self) { frames in
                tagNavReorderFrames = frames
            }
        }
    }
@@ -409,12 +453,17 @@
            VStack(spacing: 6) {
                ForEach(tagLabels) { tag in
                    SideTagPill(name: tag.name, colorIndex: tag.colorIndex,
                                dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
                                isDragging: tagNavDragItem == tag.name,
                                action: {
                        if isColorlessContainerMode {
                            toggleColorlessFill(tag.id)
                        }
                        scrollTo(tag.id)
                    })
                    .background(tagNavFrameReader(for: tag.name))
                    .zIndex(tagNavDragItem == tag.name ? 1 : 0)
                    .highPriorityGesture(tagNavReorderGesture(for: tag.name))
                    .onHover { hovering in
                        if hovering {
                            fillColorlessContainer(tag.id)
@@ -422,7 +471,12 @@
                        }
                    }
                }
            }.padding(12)
            }
            .padding(12)
            .coordinateSpace(name: "tagNavReorder")
            .onPreferenceChange(TagNavReorderFramePreferenceKey.self) { frames in
                tagNavReorderFrames = frames
            }
        }.frame(width: 135)
    }
@@ -931,27 +985,27 @@
                        VStack(spacing: 4) {
                            ForEach(sortedTagNames, id: \.self) { tagName in
                                selectableTagItem(tagName)
                                    .onDrag {
                                        dragItem = tagName
                                        return NSItemProvider(object: tagName as NSString)
                                    }
                                    .onDrop(of: [.text], isTargeted: nil) { providers, _ in
                                        guard let fromName = dragItem,
                                              var names = draggedTagNames as [String]?,
                                              let fromIdx = names.firstIndex(of: fromName),
                                              let toIdx = names.firstIndex(of: tagName),
                                              fromIdx != toIdx
                                        else { return false }
                                        let toOffset = toIdx > fromIdx ? toIdx + 1 : toIdx
                                        names.move(fromOffsets: [fromIdx], toOffset: toOffset)
                                        draggedTagNames = names
                                        TagEditor.reorderTags(names)
                                        dragItem = nil
                                        return true
                                    }
                                    .opacity(dragItem == tagName ? 0.72 : 1.0)
                                    .scaleEffect(dragItem == tagName ? 1.03 : 1.0)
                                    .background(
                                        GeometryReader { proxy in
                                            Color.clear.preference(
                                                key: TagReorderFramePreferenceKey.self,
                                                value: [tagName: proxy.frame(in: .named("tagReorderList"))]
                                            )
                                        }
                                    )
                                    .zIndex(dragItem == tagName ? 1 : 0)
                                    .highPriorityGesture(tagReorderGesture(for: tagName))
                            }
                        }.padding(12)
                    }.frame(width: 155)
                        }
                        .padding(12)
                        .coordinateSpace(name: "tagReorderList")
                        .onPreferenceChange(TagReorderFramePreferenceKey.self) { frames in
                            tagReorderFrames = frames
                        }
                    }
                    .frame(width: 155)
                }
                Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1)
@@ -1033,6 +1087,36 @@
        .onTapGesture {
            if isSelected { selectedTagNames.remove(tagName) } else { selectedTagNames.insert(tagName) }
        }
    }
    private func tagReorderGesture(for tagName: String) -> some Gesture {
        DragGesture(minimumDistance: 4, coordinateSpace: .named("tagReorderList"))
            .onChanged { value in
                if dragItem == nil {
                    dragItem = tagName
                }
                guard dragItem == tagName else { return }
                reorderDraggedTag(at: value.location)
            }
            .onEnded { _ in
                dragItem = nil
                TagEditor.reorderTags(draggedTagNames)
            }
    }
    private func reorderDraggedTag(at location: CGPoint) {
        guard let fromName = dragItem,
              let targetName = tagReorderFrames.first(where: { $0.value.contains(location) })?.key,
              fromName != targetName,
              let fromIndex = draggedTagNames.firstIndex(of: fromName),
              let toIndex = draggedTagNames.firstIndex(of: targetName)
        else { return }
        withAnimation(.easeInOut(duration: 0.14)) {
            let destination = toIndex > fromIndex ? toIndex + 1 : toIndex
            draggedTagNames.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: destination)
        }
        TagEditor.reorderTags(draggedTagNames)
    }
    private func confirmAssign() {
@@ -1146,6 +1230,78 @@
    private func toggleColorlessFill(_ id: String) {
        guard isColorlessContainerMode else { return }
        filledColorlessContainer = (filledColorlessContainer == id) ? nil : id
    }
    private func canReorderTag(_ name: String) -> Bool {
        tagColors[name] != nil && name != "Mac自带" && name != defaultGroupName
    }
    private func tagNavFrameReader(for tagName: String) -> some View {
        GeometryReader { proxy in
            Color.clear.preference(
                key: TagNavReorderFramePreferenceKey.self,
                value: canReorderTag(tagName)
                    ? [tagName: proxy.frame(in: .named("tagNavReorder"))]
                    : [:]
            )
        }
    }
    private func tagNavReorderGesture(for tagName: String) -> some Gesture {
        LongPressGesture(minimumDuration: 0.5)
            .sequenced(before: DragGesture(minimumDistance: 3, coordinateSpace: .named("tagNavReorder")))
            .onChanged { value in
                guard canReorderTag(tagName) else { return }
                switch value {
                case .first(true):
                    beginTagNavReorder(tagName)
                case .second(true, let drag?):
                    if tagNavDragItem == nil {
                        beginTagNavReorder(tagName)
                    }
                    reorderTagNavItem(at: drag.location)
                default:
                    break
                }
            }
            .onEnded { value in
                guard canReorderTag(tagName) else { return }
                if case .second(true, let drag?) = value {
                    reorderTagNavItem(at: drag.location)
                }
                endTagNavReorder()
            }
    }
    private func beginTagNavReorder(_ tagName: String) {
        guard canReorderTag(tagName) else { return }
        if tagNavDragItem == nil {
            tagNavDragItem = tagName
        }
        tagNavDragModeActive = true
    }
    private func endTagNavReorder() {
        if tagNavDragModeActive {
            TagEditor.reorderTags(draggedTagNames)
        }
        tagNavDragModeActive = false
        tagNavDragItem = nil
    }
    private func reorderTagNavItem(at location: CGPoint) {
        guard let fromName = tagNavDragItem,
              let targetName = tagNavReorderFrames.first(where: { $0.value.contains(location) })?.key,
              fromName != targetName,
              let fromIndex = draggedTagNames.firstIndex(of: fromName),
              let toIndex = draggedTagNames.firstIndex(of: targetName)
        else { return }
        withAnimation(.spring(response: 0.22, dampingFraction: 0.82)) {
            let destination = toIndex > fromIndex ? toIndex + 1 : toIndex
            draggedTagNames.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: destination)
        }
        TagEditor.reorderTags(draggedTagNames)
    }
    private func handleAppDrop(_ providers: [NSItemProvider], targetTag: String) -> Bool {
@@ -1269,16 +1425,6 @@
    func openApp(_ app: AppInfo) {
        appDragModeActive = false
        hideOverlay()
        if let bundleIdentifier = app.bundleIdentifier {
            NSWorkspace.shared.launchApplication(
                withBundleIdentifier: bundleIdentifier,
                options: [],
                additionalEventParamDescriptor: nil,
                launchIdentifier: nil
            )
            return
        }
        let configuration = NSWorkspace.OpenConfiguration()
        NSWorkspace.shared.openApplication(at: app.path, configuration: configuration)
    }
@@ -1286,6 +1432,22 @@
// MARK: - Tag Label
private struct TagReorderFramePreferenceKey: PreferenceKey {
    static var defaultValue: [String: CGRect] = [:]
    static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
        value.merge(nextValue(), uniquingKeysWith: { _, new in new })
    }
}
private struct TagNavReorderFramePreferenceKey: PreferenceKey {
    static var defaultValue: [String: CGRect] = [:]
    static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
        value.merge(nextValue(), uniquingKeysWith: { _, new in new })
    }
}
private struct TagLabel: Identifiable {
    var id: String { name }
    let name: String