diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 10:45:42 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 10:57:30 -0500 |
| commit | fbf2d9bf2cf50eb681afe18deac06a96c04004e6 (patch) | |
| tree | f337f6826c283d4246cdde824786bd18b6c770b3 /DomainDig/WatchlistView.swift | |
| parent | 6e273c2676ce29cef057d117e4427e031886e743 (diff) | |
| download | domain-dig-fbf2d9bf2cf50eb681afe18deac06a96c04004e6.tar.gz domain-dig-fbf2d9bf2cf50eb681afe18deac06a96c04004e6.tar.bz2 domain-dig-fbf2d9bf2cf50eb681afe18deac06a96c04004e6.zip | |
v4.7.0: Add watchlist tags and saved filter views
- TrackedDomain.tags: [String] (backward-compatible custom decode), with
updateTags(_:for:) and a normalized/deduplicated write path.
- Tag editing in TrackedDomainDetailView (comma-separated field, chip display).
- Tag filter chips in WatchlistView (TagFilterChipRowView), integrated into
filteredTrackedDomains alongside the existing filter/search.
- Saved views: name + snapshot the current tag/filter/sort as a
WatchlistSavedView preset (UserDefaults-backed, not part of backup/restore),
with a management sheet to apply or delete presets.
Diffstat (limited to 'DomainDig/WatchlistView.swift')
| -rw-r--r-- | DomainDig/WatchlistView.swift | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift index 43dc66f..e017502 100644 --- a/DomainDig/WatchlistView.swift +++ b/DomainDig/WatchlistView.swift @@ -10,6 +10,9 @@ struct WatchlistView: View { @State private var newTrackedDomain = "" @State private var addDomainError: String? @FocusState private var isAddDomainFieldFocused: Bool + @State private var showSavedViewsSheet = false + @State private var showSaveViewPrompt = false + @State private var newSavedViewName = "" private var pinnedDomains: [TrackedDomain] { viewModel.filteredTrackedDomains.filter(\.isPinned) @@ -23,6 +26,14 @@ struct WatchlistView: View { let _ = purchaseService.currentTier List { + if !viewModel.allWatchlistTags.isEmpty { + Section { + TagFilterChipRowView(tags: viewModel.allWatchlistTags, selection: $viewModel.watchlistTagFilter) + } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + } + if viewModel.batchLookupSource == .watchlistRefresh, (!viewModel.batchResults.isEmpty || viewModel.batchLookupRunning) { Section("Refresh Progress") { VStack(alignment: .leading, spacing: 8) { @@ -110,6 +121,17 @@ struct WatchlistView: View { } } + Button("Save Current View…") { + newSavedViewName = "" + showSaveViewPrompt = true + } + + if !viewModel.watchlistSavedViews.isEmpty { + Button("Saved Views") { + showSavedViewsSheet = true + } + } + Button(viewModel.batchLookupRunning ? "Check All Running" : "Check All") { AppHaptics.refresh() viewModel.refreshAllTrackedDomains() @@ -211,6 +233,49 @@ struct WatchlistView: View { availableDomains: viewModel.filteredTrackedDomains.map(\.domain) ) } + .alert("Save Current View", isPresented: $showSaveViewPrompt) { + TextField("View name", text: $newSavedViewName) + Button("Save") { + viewModel.saveCurrentWatchlistView(name: newSavedViewName) + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Saves the current tag, filter, and sort as a reusable preset.") + } + .sheet(isPresented: $showSavedViewsSheet) { + NavigationStack { + List { + ForEach(viewModel.watchlistSavedViews) { view in + Button { + viewModel.applyWatchlistSavedView(view) + showSavedViewsSheet = false + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(view.name) + .foregroundStyle(.primary) + Text([view.tag, view.filter.title, view.sort.title].compactMap { $0 }.joined(separator: " • ")) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + .onDelete { offsets in + viewModel.deleteWatchlistSavedViews(at: offsets) + } + } + .navigationTitle("Saved Views") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { + showSavedViewsSheet = false + } + } + ToolbarItem(placement: .topBarTrailing) { + EditButton() + } + } + } + } .preferredColorScheme(.dark) } @@ -480,6 +545,8 @@ struct TrackedDomainDetailView: View { @State private var noteDraft = "" @State private var isEditingNote = false + @State private var tagsDraft = "" + @State private var isEditingTags = false @State private var showRerunOptions = false @State private var shareEntity: ShareableEntity? @State private var showingAuditTimeline = false @@ -567,6 +634,14 @@ struct TrackedDomainDetailView: View { .disabled(!viewModel.canEdit(liveTrackedDomain)) Button { + tagsDraft = liveTrackedDomain.tags.joined(separator: ", ") + isEditingTags = true + } label: { + Label(liveTrackedDomain.tags.isEmpty ? "Add Tags" : "Edit Tags", systemImage: "tag") + } + .disabled(!viewModel.canEdit(liveTrackedDomain)) + + Button { shareEntity = .trackedDomain(liveTrackedDomain.domain) } label: { Label(liveTrackedDomain.collaboration?.isShared == true ? "Manage Share" : "Share Domain", systemImage: "person.2") @@ -574,6 +649,13 @@ struct TrackedDomainDetailView: View { } .listRowBackground(Color(.systemGray6).opacity(0.5)) + if !liveTrackedDomain.tags.isEmpty { + Section("Tags") { + TagChipRowView(tags: liveTrackedDomain.tags) + } + .listRowBackground(Color(.systemGray6).opacity(0.5)) + } + Section("Monitoring Status") { LabeledContent("State", value: viewModel.monitoringStatusLabel(for: liveTrackedDomain)) LabeledContent("Current Interval", value: viewModel.monitoringIntervalLabel(for: liveTrackedDomain)) @@ -677,6 +759,31 @@ struct TrackedDomainDetailView: View { } } } + .sheet(isPresented: $isEditingTags) { + NavigationStack { + Form { + Section("Tags") { + TextField("comma, separated, tags", text: $tagsDraft) + .textInputAutocapitalization(.never) + } + } + .navigationTitle("Edit Tags") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + isEditingTags = false + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + let tags = tagsDraft.components(separatedBy: ",") + viewModel.updateTags(tags, for: liveTrackedDomain) + isEditingTags = false + } + } + } + } + } .sheet(item: $shareEntity) { entity in CloudSharingSheet(entity: entity, title: liveTrackedDomain.domain) } |
