Backend liefert das Foto-Array unter 'media_items' (in _entry_dict), nicht 'media'. Außerdem ist is_milestone wieder eine SQLite-Int-Spalte 0/1 — der bekannte Bool-Quirk. Mit Bool? in der DTO bricht die Decoder ab und die ganze Liste verschwindet stillschweigend in den Fehler-Zustand.
140 lines
5 KiB
Swift
140 lines
5 KiB
Swift
import SwiftUI
|
|
|
|
struct TagebuchView: View {
|
|
@Environment(ActiveDogStore.self) private var activeDog
|
|
|
|
@State private var entries: [DiaryEntry] = []
|
|
@State private var isLoading = false
|
|
@State private var errorMessage: String?
|
|
@State private var showAdd = false
|
|
|
|
var body: some View {
|
|
content
|
|
.navigationTitle("Tagebuch")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button { showAdd = true } label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
.disabled(activeDog.activeDog == nil)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showAdd) {
|
|
if let dog = activeDog.activeDog {
|
|
AddDiaryEntrySheet(dogId: dog.id) { await load() }
|
|
}
|
|
}
|
|
.task {
|
|
if activeDog.dogs.isEmpty { await activeDog.loadDogs() }
|
|
await load()
|
|
}
|
|
.refreshable { await load() }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
if activeDog.activeDog == nil {
|
|
ContentUnavailableView(
|
|
"Noch kein Hund",
|
|
systemImage: "pawprint",
|
|
description: Text("Lege deinen ersten Hund in der PWA an, dann kannst du Tagebucheinträge anlegen.")
|
|
)
|
|
} else if isLoading && entries.isEmpty {
|
|
ProgressView()
|
|
} else if let errorMessage, entries.isEmpty {
|
|
ContentUnavailableView("Konnte nicht laden", systemImage: "wifi.slash", description: Text(errorMessage))
|
|
} else if entries.isEmpty {
|
|
ContentUnavailableView(
|
|
"Noch keine Einträge",
|
|
systemImage: "book",
|
|
description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.")
|
|
)
|
|
} else {
|
|
List(entries) { entry in
|
|
DiaryRow(entry: entry)
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
}
|
|
|
|
private func load() async {
|
|
guard let dog = activeDog.activeDog else { return }
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
do {
|
|
entries = try await APIClient.shared.get("/api/dogs/\(dog.id)/diary?limit=50")
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct DiaryRow: View {
|
|
let entry: DiaryEntry
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(alignment: .firstTextBaseline) {
|
|
if entry.isMilestoneFlag || entry.typ == "meilenstein" {
|
|
Image(systemName: "star.fill").foregroundStyle(.orange)
|
|
}
|
|
if let titel = entry.titel, !titel.isEmpty {
|
|
Text(titel).font(.headline)
|
|
} else {
|
|
Text("Eintrag").font(.headline).foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
if let datum = entry.datum {
|
|
Text(DiaryUtil.format(datum))
|
|
.font(.caption.monospacedDigit())
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
if let text = entry.text, !text.isEmpty {
|
|
Text(text)
|
|
.font(.subheadline)
|
|
.lineLimit(3)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
if let loc = entry.locationName, !loc.isEmpty {
|
|
Label(loc, systemImage: "mappin.and.ellipse")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
if let media = entry.mediaItems, !media.isEmpty {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 6) {
|
|
ForEach(media) { m in
|
|
AsyncImage(url: URL(string: "https://banyaro.app\(m.url)")) { phase in
|
|
switch phase {
|
|
case .success(let img): img.resizable().scaledToFill()
|
|
default: Rectangle().fill(.gray.opacity(0.15))
|
|
}
|
|
}
|
|
.frame(width: 80, height: 80)
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 6)
|
|
}
|
|
}
|
|
|
|
enum DiaryUtil {
|
|
static func format(_ str: String) -> String {
|
|
let parser = DateFormatter()
|
|
parser.locale = Locale(identifier: "en_US_POSIX")
|
|
parser.dateFormat = "yyyy-MM-dd"
|
|
if let d = parser.date(from: String(str.prefix(10))) {
|
|
let out = DateFormatter()
|
|
out.locale = Locale(identifier: "de_DE")
|
|
out.dateStyle = .medium
|
|
return out.string(from: d)
|
|
}
|
|
return str
|
|
}
|
|
}
|