feat: Add advanced tag functionality to AddBookmarkView
- Add tag search, pagination and selection with UnifiedLabelChip - Implement Set-based tag management with validation - Add keyboard toolbar with Done button - Remove field labels for compact UI - Fix duplicate toolbar buttons in ShareBookmarkView - Update localization strings for tag functionality
This commit is contained in:
parent
3aecdf9ba2
commit
61b30112ee
@ -54,7 +54,7 @@
|
|||||||
"Add" : {
|
"Add" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Add custom tag:" : {
|
"Add new tag:" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"all" : {
|
"all" : {
|
||||||
@ -73,6 +73,9 @@
|
|||||||
},
|
},
|
||||||
"All labels selected" : {
|
"All labels selected" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"All tags selected" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Archive" : {
|
"Archive" : {
|
||||||
|
|
||||||
@ -94,6 +97,9 @@
|
|||||||
},
|
},
|
||||||
"Available labels" : {
|
"Available labels" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Available tags" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Cancel" : {
|
"Cancel" : {
|
||||||
|
|
||||||
@ -121,9 +127,6 @@
|
|||||||
},
|
},
|
||||||
"Done" : {
|
"Done" : {
|
||||||
|
|
||||||
},
|
|
||||||
"e.g. work, important, later" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enter an optional title..." : {
|
"Enter an optional title..." : {
|
||||||
|
|
||||||
@ -139,9 +142,6 @@
|
|||||||
},
|
},
|
||||||
"Favorite" : {
|
"Favorite" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Fertig" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Finished reading?" : {
|
"Finished reading?" : {
|
||||||
|
|
||||||
@ -181,9 +181,6 @@
|
|||||||
},
|
},
|
||||||
"Key" : {
|
"Key" : {
|
||||||
"extractionState" : "manual"
|
"extractionState" : "manual"
|
||||||
},
|
|
||||||
"Labels" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Loading %@" : {
|
"Loading %@" : {
|
||||||
|
|
||||||
@ -281,9 +278,6 @@
|
|||||||
},
|
},
|
||||||
"Remove" : {
|
"Remove" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Required" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Reset settings" : {
|
"Reset settings" : {
|
||||||
|
|
||||||
@ -350,15 +344,9 @@
|
|||||||
},
|
},
|
||||||
"This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : {
|
"This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Title" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Unarchive Bookmark" : {
|
"Unarchive Bookmark" : {
|
||||||
|
|
||||||
},
|
|
||||||
"URL" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"URL in clipboard:" : {
|
"URL in clipboard:" : {
|
||||||
|
|
||||||
|
|||||||
@ -221,14 +221,6 @@ struct ManualTagEntryView: View {
|
|||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.toolbar {
|
|
||||||
ToolbarItemGroup(placement: .keyboard) {
|
|
||||||
Spacer()
|
|
||||||
Button("Done") {
|
|
||||||
dismissKeyboard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
addCustomTag()
|
addCustomTag()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
struct AddBookmarkView: View {
|
struct AddBookmarkView: View {
|
||||||
@State private var viewModel = AddBookmarkViewModel()
|
@State private var viewModel = AddBookmarkViewModel()
|
||||||
@ -24,18 +25,6 @@ struct AddBookmarkView: View {
|
|||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// URL Field
|
// URL Field
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
|
||||||
Label("URL", systemImage: "link")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("Required")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("https://example.com", text: $viewModel.url)
|
TextField("https://example.com", text: $viewModel.url)
|
||||||
.textFieldStyle(CustomTextFieldStyle())
|
.textFieldStyle(CustomTextFieldStyle())
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
@ -45,7 +34,7 @@ struct AddBookmarkView: View {
|
|||||||
viewModel.checkClipboard()
|
viewModel.checkClipboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clipboard Button
|
// Clipboard Button - only show if we have a URL in clipboard
|
||||||
if viewModel.showClipboardButton {
|
if viewModel.showClipboardButton {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@ -85,37 +74,128 @@ struct AddBookmarkView: View {
|
|||||||
|
|
||||||
// Title Field
|
// Title Field
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Label("Title", systemImage: "note.text")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
TextField("Optional: Custom title", text: $viewModel.title)
|
TextField("Optional: Custom title", text: $viewModel.title)
|
||||||
.textFieldStyle(CustomTextFieldStyle())
|
.textFieldStyle(CustomTextFieldStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels Field
|
// Labels Field
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Label("Labels", systemImage: "tag")
|
// Search field for tags
|
||||||
.font(.headline)
|
TextField("Search or add new tag...", text: $viewModel.searchText)
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
TextField("e.g. work, important, later", text: $viewModel.labelsText)
|
|
||||||
.textFieldStyle(CustomTextFieldStyle())
|
.textFieldStyle(CustomTextFieldStyle())
|
||||||
|
.onSubmit {
|
||||||
|
viewModel.addCustomTag()
|
||||||
|
}
|
||||||
|
|
||||||
// Labels Preview
|
// Show custom tag suggestion if search text doesn't match existing tags
|
||||||
if !viewModel.parsedLabels.isEmpty {
|
if !viewModel.searchText.isEmpty && !viewModel.allLabels.contains(where: { $0.name.lowercased() == viewModel.searchText.lowercased() }) && !viewModel.selectedLabels.contains(viewModel.searchText) {
|
||||||
LazyVGrid(columns: [
|
HStack {
|
||||||
GridItem(.adaptive(minimum: 80))
|
Text("Add new tag:")
|
||||||
], spacing: 8) {
|
.font(.caption)
|
||||||
ForEach(viewModel.parsedLabels, id: \.self) { label in
|
.foregroundColor(.secondary)
|
||||||
Text(label)
|
Text(viewModel.searchText)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Spacer()
|
||||||
|
Button(action: {
|
||||||
|
viewModel.addCustomTag()
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
Text("Add")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color.accentColor.opacity(0.1))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available labels
|
||||||
|
if !viewModel.allLabels.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text(viewModel.searchText.isEmpty ? "Available tags" : "Search results")
|
||||||
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.padding(.horizontal, 12)
|
if !viewModel.searchText.isEmpty {
|
||||||
.padding(.vertical, 6)
|
Text("(\(viewModel.filteredLabels.count) found)")
|
||||||
.background(Color.accentColor.opacity(0.1))
|
.font(.caption)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.secondary)
|
||||||
.clipShape(Capsule())
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isLabelsLoading {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
} else if viewModel.availableLabels.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("All tags selected")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
} else {
|
||||||
|
// Use pagination from ViewModel
|
||||||
|
TabView {
|
||||||
|
ForEach(Array(viewModel.availableLabelPages.enumerated()), id: \.offset) { pageIndex, labelsPage in
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(labelsPage, id: \.id) { label in
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: label.name,
|
||||||
|
isSelected: viewModel.selectedLabels.contains(label.name),
|
||||||
|
isRemovable: false,
|
||||||
|
onTap: {
|
||||||
|
viewModel.toggleLabel(label.name)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: viewModel.availableLabelPages.count > 1 ? .automatic : .never))
|
||||||
|
.frame(height: 180)
|
||||||
|
.padding(.top, -20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selected labels
|
||||||
|
if !viewModel.selectedLabels.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Selected tags")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(Array(viewModel.selectedLabels), id: \.self) { label in
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: label,
|
||||||
|
isSelected: false,
|
||||||
|
isRemovable: true,
|
||||||
|
onTap: {
|
||||||
|
// No action for selected labels
|
||||||
|
},
|
||||||
|
onRemove: {
|
||||||
|
viewModel.removeLabel(label)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@ -124,14 +204,12 @@ struct AddBookmarkView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
Spacer(minLength: 100) // Space for button
|
Spacer(minLength: 120) // Space for button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom Action Area
|
// Bottom Action Area
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Divider()
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
// Save Button
|
// Save Button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@ -161,13 +239,6 @@ struct AddBookmarkView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||||
|
|
||||||
// Cancel Button
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
viewModel.clearForm()
|
|
||||||
}
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
@ -189,10 +260,22 @@ struct AddBookmarkView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.errorMessage ?? "Unknown error")
|
Text(viewModel.errorMessage ?? "Unknown error")
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .keyboard) {
|
||||||
|
Spacer()
|
||||||
|
Button("Done") {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.keyboard)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.checkClipboard()
|
viewModel.checkClipboard()
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadAllLabels()
|
||||||
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewModel.clearForm()
|
viewModel.clearForm()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,18 @@ import UIKit
|
|||||||
@Observable
|
@Observable
|
||||||
class AddBookmarkViewModel {
|
class AddBookmarkViewModel {
|
||||||
private let createBookmarkUseCase = DefaultUseCaseFactory.shared.makeCreateBookmarkUseCase()
|
private let createBookmarkUseCase = DefaultUseCaseFactory.shared.makeCreateBookmarkUseCase()
|
||||||
|
private let getLabelsUseCase = DefaultUseCaseFactory.shared.makeGetLabelsUseCase()
|
||||||
|
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
var title: String = ""
|
var title: String = ""
|
||||||
var labelsText: String = ""
|
var labelsText: String = ""
|
||||||
|
|
||||||
|
// Tag functionality
|
||||||
|
var allLabels: [BookmarkLabel] = []
|
||||||
|
var selectedLabels: Set<String> = []
|
||||||
|
var searchText: String = ""
|
||||||
|
var isLabelsLoading: Bool = false
|
||||||
|
|
||||||
var isLoading: Bool = false
|
var isLoading: Bool = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var showErrorAlert: Bool = false
|
var showErrorAlert: Bool = false
|
||||||
@ -28,6 +35,79 @@ class AddBookmarkViewModel {
|
|||||||
.filter { !$0.isEmpty }
|
.filter { !$0.isEmpty }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computed properties for tag functionality
|
||||||
|
var availableLabels: [BookmarkLabel] {
|
||||||
|
return allLabels.filter { !selectedLabels.contains($0.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredLabels: [BookmarkLabel] {
|
||||||
|
if searchText.isEmpty {
|
||||||
|
return availableLabels
|
||||||
|
} else {
|
||||||
|
return availableLabels.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableLabelPages: [[BookmarkLabel]] {
|
||||||
|
let pageSize = Constants.Labels.pageSize
|
||||||
|
let labelsToShow = searchText.isEmpty ? availableLabels : filteredLabels
|
||||||
|
|
||||||
|
if labelsToShow.count <= pageSize {
|
||||||
|
return [labelsToShow]
|
||||||
|
} else {
|
||||||
|
return stride(from: 0, to: labelsToShow.count, by: pageSize).map {
|
||||||
|
Array(labelsToShow[$0..<min($0 + pageSize, labelsToShow.count)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadAllLabels() async {
|
||||||
|
isLabelsLoading = true
|
||||||
|
defer { isLabelsLoading = false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let labels = try await getLabelsUseCase.execute()
|
||||||
|
allLabels = labels.sorted { $0.count > $1.count }
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Failed to load labels"
|
||||||
|
showErrorAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func addCustomTag() {
|
||||||
|
let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
|
|
||||||
|
let lowercased = trimmed.lowercased()
|
||||||
|
let allExisting = Set(allLabels.map { $0.name.lowercased() })
|
||||||
|
let allSelected = Set(selectedLabels.map { $0.lowercased() })
|
||||||
|
|
||||||
|
if allExisting.contains(lowercased) || allSelected.contains(lowercased) {
|
||||||
|
// Tag already exists, don't add
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
selectedLabels.insert(trimmed)
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func toggleLabel(_ label: String) {
|
||||||
|
if selectedLabels.contains(label) {
|
||||||
|
selectedLabels.remove(label)
|
||||||
|
} else {
|
||||||
|
selectedLabels.insert(label)
|
||||||
|
}
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func removeLabel(_ label: String) {
|
||||||
|
selectedLabels.remove(label)
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func createBookmark() async {
|
func createBookmark() async {
|
||||||
guard isValid else { return }
|
guard isValid else { return }
|
||||||
@ -39,7 +119,7 @@ class AddBookmarkViewModel {
|
|||||||
do {
|
do {
|
||||||
let cleanURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
let cleanURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let cleanTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let cleanTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let labels = parsedLabels
|
let labels = Array(selectedLabels)
|
||||||
|
|
||||||
let request = CreateBookmarkRequest(
|
let request = CreateBookmarkRequest(
|
||||||
url: cleanURL,
|
url: cleanURL,
|
||||||
@ -97,6 +177,8 @@ class AddBookmarkViewModel {
|
|||||||
url = ""
|
url = ""
|
||||||
title = ""
|
title = ""
|
||||||
labelsText = ""
|
labelsText = ""
|
||||||
|
selectedLabels.removeAll()
|
||||||
|
searchText = ""
|
||||||
clipboardURL = nil
|
clipboardURL = nil
|
||||||
showClipboardButton = false
|
showClipboardButton = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,27 +18,13 @@ struct BookmarkLabelsView: View {
|
|||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
// Add new label with search functionality
|
// Add new label with search functionality
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
HStack(spacing: 12) {
|
TextField("Search or add new tag...", text: $viewModel.searchText)
|
||||||
TextField("Search or add new tag...", text: $viewModel.searchText)
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.onSubmit {
|
||||||
.onSubmit {
|
|
||||||
Task {
|
|
||||||
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
Task {
|
Task {
|
||||||
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
|
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Image(systemName: "plus.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
}
|
}
|
||||||
.disabled(viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show custom tag suggestion if search text doesn't match existing tags
|
// Show custom tag suggestion if search text doesn't match existing tags
|
||||||
if !viewModel.searchText.isEmpty && !viewModel.filteredLabels.contains(where: { $0.name.lowercased() == viewModel.searchText.lowercased() }) {
|
if !viewModel.searchText.isEmpty && !viewModel.filteredLabels.contains(where: { $0.name.lowercased() == viewModel.searchText.lowercased() }) {
|
||||||
@ -50,18 +36,25 @@ struct BookmarkLabelsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Add") {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
|
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
Text("Add")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 12)
|
||||||
.background(Color.accentColor.opacity(0.1))
|
.background(Color.accentColor.opacity(0.1))
|
||||||
.cornerRadius(6)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user