Schnellnotiz und Editorfenster.
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct QuickNoteView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(ConnectivityMonitor.self) private var connectivity
|
||||
|
||||
// Form fields
|
||||
@State private var title = ""
|
||||
@State private var content = ""
|
||||
@State private var tagsRaw = ""
|
||||
|
||||
// Location selection
|
||||
@State private var shelves: [ShelfDTO] = []
|
||||
@State private var books: [BookDTO] = []
|
||||
@State private var selectedShelf: ShelfDTO? = nil
|
||||
@State private var selectedBook: BookDTO? = nil
|
||||
@State private var isLoadingShelves = false
|
||||
@State private var isLoadingBooks = false
|
||||
|
||||
// Save state
|
||||
@State private var isSaving = false
|
||||
@State private var savedMessage: String? = nil
|
||||
@State private var error: String? = nil
|
||||
|
||||
// Pending notes
|
||||
@Query(sort: \PendingNote.createdAt) private var pendingNotes: [PendingNote]
|
||||
@State private var isUploadingPending = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Note content
|
||||
Section(L("quicknote.field.title")) {
|
||||
TextField(L("quicknote.field.title"), text: $title)
|
||||
}
|
||||
|
||||
Section(L("quicknote.field.content")) {
|
||||
TextEditor(text: $content)
|
||||
.frame(minHeight: 120)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
|
||||
Section(L("quicknote.field.tags")) {
|
||||
TextField(L("quicknote.field.tags"), text: $tagsRaw)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
}
|
||||
|
||||
// Location: shelf → book
|
||||
Section(L("quicknote.section.location")) {
|
||||
if isLoadingShelves {
|
||||
HStack {
|
||||
ProgressView().controlSize(.small)
|
||||
Text(L("quicknote.shelf.loading"))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
} else {
|
||||
Picker(L("create.shelf.title"), selection: $selectedShelf) {
|
||||
Text(L("quicknote.shelf.none")).tag(ShelfDTO?.none)
|
||||
ForEach(shelves) { shelf in
|
||||
Text(shelf.name).tag(ShelfDTO?.some(shelf))
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedShelf) { _, shelf in
|
||||
selectedBook = nil
|
||||
if let shelf {
|
||||
Task { await loadBooks(for: shelf) }
|
||||
} else {
|
||||
Task { await loadAllBooks() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isLoadingBooks {
|
||||
HStack {
|
||||
ProgressView().controlSize(.small)
|
||||
Text(L("quicknote.book.loading"))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
} else {
|
||||
Picker(L("create.book.title"), selection: $selectedBook) {
|
||||
Text(L("quicknote.book.none")).tag(BookDTO?.none)
|
||||
ForEach(books) { book in
|
||||
Text(book.name).tag(BookDTO?.some(book))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback
|
||||
if let msg = savedMessage {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
|
||||
Text(msg).foregroundStyle(.secondary).font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let err = error {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.red)
|
||||
Text(err).foregroundStyle(.red).font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pending notes section
|
||||
if !pendingNotes.isEmpty {
|
||||
Section {
|
||||
ForEach(pendingNotes) { note in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(note.title).font(.body)
|
||||
Text(note.bookName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deletePending)
|
||||
|
||||
Button {
|
||||
Task { await uploadPending() }
|
||||
} label: {
|
||||
if isUploadingPending {
|
||||
HStack {
|
||||
ProgressView().controlSize(.small)
|
||||
Text(L("quicknote.pending.uploading"))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 6)
|
||||
}
|
||||
} else {
|
||||
Label(L("quicknote.pending.upload"), systemImage: "arrow.up.circle")
|
||||
}
|
||||
}
|
||||
.disabled(isUploadingPending || !connectivity.isConnected)
|
||||
} header: {
|
||||
Text(L("quicknote.pending.title"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L("quicknote.title"))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
if isSaving {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Button(L("quicknote.save")) {
|
||||
Task { await save() }
|
||||
}
|
||||
.disabled(title.isEmpty || selectedBook == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadShelves()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Load shelves / books
|
||||
|
||||
private func loadShelves() async {
|
||||
isLoadingShelves = true
|
||||
shelves = (try? await BookStackAPI.shared.fetchShelves()) ?? []
|
||||
isLoadingShelves = false
|
||||
await loadAllBooks()
|
||||
}
|
||||
|
||||
private func loadAllBooks() async {
|
||||
isLoadingBooks = true
|
||||
books = (try? await BookStackAPI.shared.fetchBooks()) ?? []
|
||||
isLoadingBooks = false
|
||||
}
|
||||
|
||||
private func loadBooks(for shelf: ShelfDTO) async {
|
||||
isLoadingBooks = true
|
||||
books = (try? await BookStackAPI.shared.fetchShelf(id: shelf.id))?.books ?? []
|
||||
isLoadingBooks = false
|
||||
}
|
||||
|
||||
// MARK: - Save
|
||||
|
||||
private func save() async {
|
||||
guard let book = selectedBook else {
|
||||
error = L("quicknote.error.nobook")
|
||||
return
|
||||
}
|
||||
error = nil
|
||||
savedMessage = nil
|
||||
isSaving = true
|
||||
|
||||
let tagDTOs = parsedTags()
|
||||
|
||||
if connectivity.isConnected {
|
||||
do {
|
||||
let page = try await BookStackAPI.shared.createPage(
|
||||
bookId: book.id,
|
||||
name: title,
|
||||
markdown: content,
|
||||
tags: tagDTOs
|
||||
)
|
||||
AppLog(.info, "Quick note '\(title)' created as page \(page.id)", category: "QuickNote")
|
||||
savedMessage = L("quicknote.saved.online")
|
||||
resetForm()
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
} else {
|
||||
let tagsString = tagDTOs.map { "\($0.name):\($0.value)" }.joined(separator: ",")
|
||||
let pending = PendingNote(
|
||||
title: title,
|
||||
markdown: content,
|
||||
tags: tagsString,
|
||||
bookId: book.id,
|
||||
bookName: book.name
|
||||
)
|
||||
modelContext.insert(pending)
|
||||
try? modelContext.save()
|
||||
savedMessage = L("quicknote.saved.offline")
|
||||
resetForm()
|
||||
}
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
// MARK: - Pending
|
||||
|
||||
private func uploadPending() async {
|
||||
isUploadingPending = true
|
||||
await SyncService.shared.flushPendingNotes(context: modelContext)
|
||||
isUploadingPending = false
|
||||
}
|
||||
|
||||
private func deletePending(at offsets: IndexSet) {
|
||||
for i in offsets {
|
||||
modelContext.delete(pendingNotes[i])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func parsedTags() -> [TagDTO] {
|
||||
tagsRaw.split(separator: ",").compactMap { token -> TagDTO? in
|
||||
let parts = token.trimmingCharacters(in: .whitespaces)
|
||||
.split(separator: ":", maxSplits: 1)
|
||||
.map(String.init)
|
||||
guard parts.count == 2 else { return nil }
|
||||
return TagDTO(name: parts[0].trimmingCharacters(in: .whitespaces),
|
||||
value: parts[1].trimmingCharacters(in: .whitespaces),
|
||||
order: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetForm() {
|
||||
title = ""
|
||||
content = ""
|
||||
tagsRaw = ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user