Ariver
2026-06-30 70a1cc2e63d2c49a135bd89cec3ffb1d427d90ee
src/Apptag/TagEditorView.swift
@@ -1,3 +1,4 @@
import AppKit
import SwiftUI
// MARK: - Reusable Tag Editor (overlay edit mode + Preferences)
@@ -6,7 +7,11 @@
/// All changes (rename, delete, setColor) are saved to the local tag database.
struct TagEditorView: View {
    @Binding var tagColors: [String: Int]
    @Binding var tagCustomColors: [String: TagCustomColor]
    let excludedTagNames: Set<String>
    let isCustomColorUnlocked: Bool
    let onLockedCustomColor: () -> Void
    var topLeadingAccessory: AnyView? = nil
    var onRefresh: (() -> Void)?
    @State private var editingTagName: String? = nil
@@ -30,7 +35,13 @@
        VStack(spacing: 0) {
            // "New Tag" button at top
            HStack {
                Spacer()
                if let topLeadingAccessory {
                    topLeadingAccessory
                        .frame(maxWidth: .infinity, alignment: .leading)
                } else {
                    Spacer()
                }
                Button {
                    addingNewTag = true
                    newTagNameText = ""
@@ -85,16 +96,20 @@
    private func tagEditRow(_ tagName: String) -> some View {
        let colorIndex = tagColors[tagName] ?? 0
        let hasCustomColor = tagCustomColors[tagName] != nil
        return HStack(spacing: 10) {
            HStack(spacing: 4) {
                ForEach(TagColor.allIndices, id: \.self) { idx in
                    ColorSwatch(index: idx, isSelected: idx == colorIndex) {
                    ColorSwatch(index: idx, isSelected: !hasCustomColor && idx == colorIndex) {
                        TagEditor.setColor(idx, for: tagName)
                        tagColors[tagName] = idx
                        tagCustomColors.removeValue(forKey: tagName)
                        onRefresh?()
                    }
                }
            }
            customColorControl(tagName: tagName, colorIndex: colorIndex)
            if editingTagName == tagName {
                MacTextField(
@@ -167,6 +182,8 @@
        TagEditor.renameTag(from: oldName, to: newName)
        tagColors[newName] = tagColors[oldName]
        tagColors.removeValue(forKey: oldName)
        tagCustomColors[newName] = tagCustomColors[oldName]
        tagCustomColors.removeValue(forKey: oldName)
        editingTagName = nil
        onRefresh?()
    }
@@ -184,6 +201,82 @@
    private func deleteTag(_ name: String) {
        TagEditor.deleteTagCompletely(name)
        tagColors.removeValue(forKey: name)
        tagCustomColors.removeValue(forKey: name)
        onRefresh?()
    }
    @ViewBuilder
    private func customColorControl(tagName: String, colorIndex: Int) -> some View {
        let isSelected = tagCustomColors[tagName] != nil
        if isCustomColorUnlocked {
            ColorPicker(
                "",
                selection: customColorBinding(tagName: tagName, colorIndex: colorIndex),
                supportsOpacity: false
            )
            .labelsHidden()
            .opacity(0.02)
            .frame(width: 64, height: 24)
            .overlay(
                customColorSwatch(tagName: tagName, colorIndex: colorIndex, isSelected: isSelected)
                    .allowsHitTesting(false)
            )
            .frame(width: 72, alignment: .leading)
            .help(tr("tag.customColor"))
        } else {
            Button(action: onLockedCustomColor) {
                customColorSwatch(tagName: tagName, colorIndex: colorIndex, isSelected: isSelected)
            }
            .buttonStyle(.plain)
            .frame(width: 72, alignment: .leading)
            .help(tr("pro.prompt.customTagColors"))
        }
    }
    private func customColorSwatch(tagName: String, colorIndex: Int, isSelected: Bool) -> some View {
        let fill = Color(nsColor: TagColor.nsColor(for: colorIndex, customColor: tagCustomColors[tagName]))
        return ZStack {
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .fill(fill)
                .frame(width: 58, height: 18)
                .overlay(
                    RoundedRectangle(cornerRadius: 10, style: .continuous)
                        .stroke(Color.secondary.opacity(0.18), lineWidth: 1)
                )
                .padding(3)
                .overlay(
                    RoundedRectangle(cornerRadius: 13, style: .continuous)
                        .stroke(isSelected ? Color.white : Color.clear, lineWidth: 2)
                )
                .shadow(color: .black.opacity(0.15), radius: 2, y: 1)
            Image(systemName: "crown")
                .font(.system(size: 10, weight: .semibold))
                .symbolRenderingMode(.monochrome)
                .foregroundStyle(Color.white)
                .shadow(color: .black.opacity(0.28), radius: 1, y: 0.5)
        }
    }
    private func customColorBinding(tagName: String, colorIndex: Int) -> Binding<Color> {
        Binding(
            get: {
                Color(nsColor: TagColor.nsColor(for: colorIndex, customColor: tagCustomColors[tagName]))
            },
            set: { color in
                guard isCustomColorUnlocked else {
                    onLockedCustomColor()
                    return
                }
                let nsColor = NSColor(color)
                let customColor = TagCustomColor(nsColor: nsColor)
                guard TagEditor.setCustomColor(customColor, for: tagName) else {
                    onLockedCustomColor()
                    return
                }
                tagCustomColors[tagName] = customColor
                onRefresh?()
            }
        )
    }
}