| | |
| | | @State private var newTagColorIndex = 0 |
| | | |
| | | private var sortedTagNames: [String] { |
| | | tagColors.keys |
| | | .filter { !excludedTagNames.contains($0) } |
| | | .sorted { $0.localizedStandardCompare($1) == .orderedAscending } |
| | | let order = TagEditor.orderedTagNames() |
| | | let filtered = tagColors.keys.filter { !excludedTagNames.contains($0) } |
| | | let ordered = order.filter { filtered.contains($0) } |
| | | let remaining = filtered.filter { !ordered.contains($0) }.sorted() |
| | | return ordered + remaining |
| | | } |
| | | |
| | | var body: some View { |
| | |
| | | newTagNameText = "" |
| | | newTagColorIndex = 0 |
| | | } label: { |
| | | Label("New Tag", systemImage: "plus") |
| | | Label(tr("tag.newTag"), systemImage: "plus") |
| | | .font(.system(size: 12)) |
| | | } |
| | | .buttonStyle(.bordered) |
| | |
| | | ScrollView { |
| | | VStack(spacing: 0) { |
| | | if sortedTagNames.isEmpty && !addingNewTag { |
| | | Text("No tags yet. Click \"New Tag\" to create one.") |
| | | Text(tr("edit.noTags")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .padding(.vertical, 40) |
| | | } |
| | | ForEach(sortedTagNames, id: \.self) { tagName in |
| | | tagEditRow(tagName) |
| | | Divider().opacity(0.15).padding(.leading, 16) |
| | | } |
| | | if addingNewTag { |
| | | newTagRow() |
| | | Divider().opacity(0.15).padding(.leading, 16) |
| | | } |
| | | ForEach(sortedTagNames, id: \.self) { tagName in |
| | | tagEditRow(tagName) |
| | | Divider().opacity(0.15).padding(.leading, 16) |
| | | } |
| | | } |
| | |
| | | if editingTagName == tagName { |
| | | MacTextField( |
| | | text: $editingTagText, |
| | | placeholder: "Tag name", |
| | | placeholder: tr("tag.name"), |
| | | onSubmit: { commitTagRename(tagName) } |
| | | ) |
| | | .frame(width: 160, height: 24) |
| | | Button("Save") { commitTagRename(tagName) } |
| | | Button(tr("tag.save")) { commitTagRename(tagName) } |
| | | .buttonStyle(.borderedProminent).controlSize(.small) |
| | | Button("Cancel") { editingTagName = nil } |
| | | Button(tr("tag.cancel")) { editingTagName = nil } |
| | | .buttonStyle(.plain).foregroundStyle(.secondary) |
| | | } else { |
| | | Text(tagName).font(.system(size: 14)).frame(width: 160, alignment: .leading) |
| | |
| | | } |
| | | MacTextField( |
| | | text: $newTagNameText, |
| | | placeholder: "New tag name", |
| | | placeholder: tr("tag.newName"), |
| | | onSubmit: { addNewTag() } |
| | | ) |
| | | .frame(width: 160, height: 24) |
| | | Button("Add") { addNewTag() } |
| | | Button(tr("tag.add")) { addNewTag() } |
| | | .buttonStyle(.borderedProminent).controlSize(.small) |
| | | Button("Cancel") { addingNewTag = false; newTagNameText = "" } |
| | | Button(tr("tag.cancel")) { addingNewTag = false; newTagNameText = "" } |
| | | .buttonStyle(.plain).foregroundStyle(.secondary) |
| | | Spacer() |
| | | } |
| | |
| | | tagColors[newName] = tagColors[oldName] |
| | | tagColors.removeValue(forKey: oldName) |
| | | editingTagName = nil |
| | | // Don't call onRefresh — re-scan may re-discover old tag from |
| | | // SIP-protected apps where xattr write silently fails. |
| | | onRefresh?() |
| | | } |
| | | |
| | | /// Add a new tag name+color and persist to the database. |