| | |
| | | import AppKit |
| | | import SwiftUI |
| | | |
| | | // MARK: - Reusable Tag Editor (overlay edit mode + Preferences) |
| | |
| | | /// 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 |
| | |
| | | VStack(spacing: 0) { |
| | | // "New Tag" button at top |
| | | HStack { |
| | | if let topLeadingAccessory { |
| | | topLeadingAccessory |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | } else { |
| | | Spacer() |
| | | } |
| | | |
| | | Button { |
| | | addingNewTag = true |
| | | newTagNameText = "" |
| | |
| | | |
| | | 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( |
| | |
| | | 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?() |
| | | } |
| | |
| | | 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?() |
| | | } |
| | | ) |
| | | } |
| | | } |