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) } }