340770861e
Each view now uses themeManager.current.background and secondaryBackground explicitly, matching the pattern in SetupView. preferredColorScheme alone only toggles system dark/light — specific hex colors require explicit background modifiers.
139 lines
4.7 KiB
Swift
139 lines
4.7 KiB
Swift
import SwiftUI
|
|
|
|
struct NoteListView: View {
|
|
@EnvironmentObject var client: WebDAVClient
|
|
@State private var searchText = ""
|
|
@State private var themeManager = ThemeManager.shared
|
|
let onSettings: () -> Void
|
|
|
|
private var filteredNotes: [DenoteNote] {
|
|
guard !searchText.isEmpty else { return client.notes }
|
|
let q = searchText.lowercased()
|
|
return client.notes.filter {
|
|
$0.title.lowercased().contains(q) ||
|
|
$0.keywords.joined(separator: " ").lowercased().contains(q) ||
|
|
($0.content?.lowercased().contains(q) == true)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 0) {
|
|
searchBar
|
|
if client.isLoading {
|
|
Spacer()
|
|
ProgressView("Loading notes...")
|
|
Spacer()
|
|
} else if let error = client.errorMessage {
|
|
errorView(message: error)
|
|
} else {
|
|
notesList
|
|
}
|
|
}
|
|
.background(themeManager.current.background)
|
|
.toolbarBackground(themeManager.current.secondaryBackground, for: .navigationBar)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button { onSettings() } label: {
|
|
Image(systemName: "gearshape")
|
|
}
|
|
}
|
|
if client.isPrefetching {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
HStack(spacing: 6) {
|
|
ProgressView().scaleEffect(0.75)
|
|
Text("Indexing").font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.task { await client.loadNotes() }
|
|
}
|
|
|
|
private var searchBar: some View {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundStyle(.secondary)
|
|
TextField("Search notes", text: $searchText)
|
|
.autocorrectionDisabled()
|
|
if !searchText.isEmpty {
|
|
Button { searchText = "" } label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 9)
|
|
.background(themeManager.current.secondaryBackground)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 8)
|
|
.background(themeManager.current.background)
|
|
}
|
|
|
|
private var notesList: some View {
|
|
List(filteredNotes) { note in
|
|
NavigationLink(destination: NoteDetailView(note: note)) {
|
|
NoteRowView(note: note)
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.scrollContentBackground(.hidden)
|
|
.background(themeManager.current.background)
|
|
.refreshable { await client.loadNotes() }
|
|
.overlay {
|
|
if !searchText.isEmpty && filteredNotes.isEmpty {
|
|
ContentUnavailableView.search(text: searchText)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func errorView(message: String) -> some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "wifi.exclamationmark")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.secondary)
|
|
Text(message)
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(.secondary)
|
|
Button("Retry") { Task { await client.loadNotes() } }
|
|
.buttonStyle(.bordered)
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
struct NoteRowView: View {
|
|
let note: DenoteNote
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(note.displayTitle)
|
|
.font(.headline)
|
|
.lineLimit(2)
|
|
if !note.keywords.isEmpty {
|
|
HStack(spacing: 4) {
|
|
ForEach(note.keywords, id: \.self) { keyword in
|
|
Text(keyword)
|
|
.font(.caption2)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color.accentColor.opacity(0.15))
|
|
.foregroundStyle(Color.accentColor)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
}
|
|
if !note.formattedDate.isEmpty {
|
|
Text(note.formattedDate)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
}
|