Tagebuch: Detail-Ansicht + tappbare Listen-Zeilen
DiaryDetailView mit Header (Datum, Meilenstein-Badge, Ort), Foto-
Galerie als TabView mit Page-Indicator, Volltext, Tags als Chips,
Mini-Karte für GPS-Eintrag mit Tap nach Apple Maps, und 'Eintrag
löschen' mit Bestätigungs-Alert → DELETE /api/dogs/{id}/diary/{eid}.
Liste in TagebuchView jetzt mit NavigationLink statt nur Anzeige.
App-Store-Material:
- AppStore/marketing.md mit Name, Untertitel, Beschreibung 2700
Zeichen, Keywords, Privacy-Antworten, Reviewer-Notiz
- AppStore/screenshots/ — 8 Stück 1320x2868 (iPhone 17 Pro Max 6.9'')
280
AppStore/marketing.md
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
# App Store Connect — Metadaten für Ban Yaro Go
|
||||||
|
|
||||||
|
Alle Texte sind so geschrieben, dass sie an den Apple-Zeichenlimits sitzen
|
||||||
|
und die Funktionen abbilden, die wirklich in der App sind. Wenn du was
|
||||||
|
ändern willst — am besten direkt hier editieren, dann hast du es für die
|
||||||
|
nächste Version griffbereit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App-Name (max. 30 Zeichen)
|
||||||
|
|
||||||
|
**Ban Yaro Go** *(11 Zeichen)*
|
||||||
|
|
||||||
|
## Untertitel (max. 30 Zeichen — wird unter dem Namen angezeigt)
|
||||||
|
|
||||||
|
Zur Auswahl, alle ≤30:
|
||||||
|
|
||||||
|
1. **Gassi-Tracker für Hundefans** *(27)* ← Empfehlung
|
||||||
|
2. **Gassi-Touren tracken & teilen** *(29)*
|
||||||
|
3. **Touren, Wetter, Treffen — Hund** *(30)*
|
||||||
|
4. **Dein Hund unterwegs** *(20)*
|
||||||
|
|
||||||
|
## Kategorien
|
||||||
|
|
||||||
|
- **Primär:** Gesundheit & Fitness
|
||||||
|
- **Sekundär:** Lifestyle
|
||||||
|
|
||||||
|
*(Alternative für Primär: „Lifestyle", wenn die Fitness-Aspekte zu schmal
|
||||||
|
wirken. Apples Reviewer akzeptiert beides — Gesundheit & Fitness ranked
|
||||||
|
in der Regel besser, weil HealthKit aktiv genutzt wird.)*
|
||||||
|
|
||||||
|
## Altersfreigabe
|
||||||
|
|
||||||
|
**4+** — keine sensiblen Inhalte. Beim App-Store-Fragebogen alles auf
|
||||||
|
„Keine" stellen außer:
|
||||||
|
- „Unbeschränkter Web-Zugriff": Nein (banyaro.app ist eingebettete Linkliste, keine WebView)
|
||||||
|
- „User-Generated Content": **Ja, selten** (Fotos, Tagebuch, Forumsverweis) — Apple verlangt dann „Moderation vorhanden" — auf banyaro.app gibt's Moderation, das passt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Werbender Text / Promotional Text (max. 170 Zeichen)
|
||||||
|
|
||||||
|
*(Kann nach Veröffentlichung jederzeit ohne Re-Submission geändert werden — z. B. für Aktionen oder neue Features.)*
|
||||||
|
|
||||||
|
> Lass dich beim Gassi nicht ablenken — Ban Yaro Go zeichnet deine Tour auf, warnt vor Giftködern und zeigt das Wetter, das wirklich für deinen Hund passt.
|
||||||
|
|
||||||
|
*(167 Zeichen)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beschreibung (max. 4000 Zeichen)
|
||||||
|
|
||||||
|
```
|
||||||
|
Ban Yaro Go ist die native iOS-Begleit-App zu banyaro.app, der Community
|
||||||
|
für Hundefreund:innen. Unterwegs zählt nicht jede Funktion — sondern
|
||||||
|
das, was du wirklich brauchst, wenn du draußen bist.
|
||||||
|
|
||||||
|
|
||||||
|
WAS DU UNTERWEGS BEKOMMST
|
||||||
|
|
||||||
|
• Gassi-Tour aufzeichnen — GPS-Track im Hintergrund, sogar bei
|
||||||
|
gesperrtem Bildschirm, mit Live-Aktivität in der Dynamischen Insel
|
||||||
|
und automatischer Pause beim Stehenbleiben
|
||||||
|
|
||||||
|
• Apple Health Integration — auf Wunsch wird jede Tour als
|
||||||
|
Spaziergang-Workout mit Route in Apple Health geschrieben
|
||||||
|
|
||||||
|
• Fotos vom Spaziergang — direkt mit Geolocation, hängen am
|
||||||
|
richtigen Streckenpunkt und landen im Tagebuch deines Hundes
|
||||||
|
|
||||||
|
• Gassi-Wetter — speziell für Hunde: gefühlte Asphalttemperatur,
|
||||||
|
Zecken-Warnung, UV-Index, Niederschlagsverlauf nächste Stunden,
|
||||||
|
basierend auf Apples WeatherKit
|
||||||
|
|
||||||
|
• Giftköder-Karte — andere Hundefreund:innen melden gesichtete
|
||||||
|
Köder mit Position und Foto, du siehst sie als Pin auf der Karte
|
||||||
|
|
||||||
|
|
||||||
|
GEMEINSAM UNTERWEGS
|
||||||
|
|
||||||
|
• Gassi-Treffen planen — Termin und Treffpunkt festlegen, andere
|
||||||
|
in der Nähe sehen das und können beitreten, alles mit Navigation
|
||||||
|
in Apple Maps
|
||||||
|
|
||||||
|
• Stamm-Gassi-Zeiten — deine regelmäßigen Runden eintragen, lokale
|
||||||
|
Erinnerungen auf dem iPhone, andere finden Verabredungen über
|
||||||
|
banyaro.app
|
||||||
|
|
||||||
|
• Verlorene Hunde — Melde-Pin mit Foto und Beschreibung in
|
||||||
|
der Umgebung, damit Suchhilfe schnell ankommt
|
||||||
|
|
||||||
|
|
||||||
|
ZUHAUSE
|
||||||
|
|
||||||
|
• Heim-Tab mit täglich wechselndem Hundebild und Schnellzugriff
|
||||||
|
auf deine wichtigsten Listen
|
||||||
|
|
||||||
|
• Tagebuch — Erlebnisse, Meilensteine, Fotos und GPS-Punkte zu
|
||||||
|
deinem Hund festhalten
|
||||||
|
|
||||||
|
• Ausgaben — Futter, Tierarzt, Spielzeug nach Kategorien
|
||||||
|
protokollieren
|
||||||
|
|
||||||
|
• GPX-Import — Tracks aus anderen Apps (Komoot, Outdooractive,
|
||||||
|
Files-App) per Teilen-Menü als Tour übernehmen
|
||||||
|
|
||||||
|
|
||||||
|
PRIVAT, KOSTENLOS, OPTIONAL
|
||||||
|
|
||||||
|
Ban Yaro Go ist Teil von banyaro.app — der gemeinnützigen Community
|
||||||
|
für Hundefreund:innen. Die Basis ist kostenlos, dauerhaft, ohne
|
||||||
|
Werbung. Premium-Funktionen (z. B. erweiterte Statistiken oder
|
||||||
|
unbegrenzte Cloud-Touren) kannst du auf banyaro.app buchen — die App
|
||||||
|
auf dem iPhone bleibt unabhängig nutzbar.
|
||||||
|
|
||||||
|
Hinweis zum Standort: Ban Yaro Go braucht „Standort: Immer", damit
|
||||||
|
deine Tour weiterläuft, wenn das iPhone in der Hosentasche steckt
|
||||||
|
oder der Bildschirm aus geht. Die Daten bleiben auf deinem Gerät und
|
||||||
|
landen nur in deinem banyaro-Konto, wenn du die Tour speicherst.
|
||||||
|
|
||||||
|
|
||||||
|
banyaro.app im Browser ergänzt diese App um die Sachen, die am
|
||||||
|
großen Bildschirm besser sind: ausführliche Tour-Karten, Forum,
|
||||||
|
Hunde-Profil-Pflege, Statistik und mehr.
|
||||||
|
|
||||||
|
Datenschutz: banyaro.app/datenschutz
|
||||||
|
Support: banyaro.app/help
|
||||||
|
```
|
||||||
|
|
||||||
|
*(ca. 2700 Zeichen — Apple-Limit ist 4000)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keywords (max. 100 Zeichen, kommagetrennt)
|
||||||
|
|
||||||
|
```
|
||||||
|
hund,gassi,tracker,gps,tour,hundebesitzer,hundewetter,giftköder,tagebuch,community,workout
|
||||||
|
```
|
||||||
|
|
||||||
|
*(96 Zeichen — bewusst knapp unter Limit. Apple zählt Leerzeichen mit; deshalb keine.)*
|
||||||
|
|
||||||
|
**Tipps zur Auswahl:**
|
||||||
|
- „Hund" und „Gassi" sind die wichtigsten Anker
|
||||||
|
- „Tracker" + „GPS" decken die Tracking-Funktion ab
|
||||||
|
- „Giftköder" ist ein USP — selten in anderen Apps
|
||||||
|
- „Hundewetter" — Komposita-Keyword das wenig Konkurrenz hat
|
||||||
|
- App-Name darf NICHT in Keywords (Apple bezieht ihn automatisch ein)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support- und Marketing-URLs
|
||||||
|
|
||||||
|
- **Support URL** (Pflicht): `https://banyaro.app/help`
|
||||||
|
- **Marketing URL** (optional): `https://banyaro.app`
|
||||||
|
- **Datenschutz-URL** (Pflicht): `https://banyaro.app/datenschutz`
|
||||||
|
|
||||||
|
*(Wenn die Help/Datenschutz-Seiten noch nicht existieren, baue sie auf
|
||||||
|
banyaro.app — die App referenziert sie direkt aus Settings + Reviewer-
|
||||||
|
Notiz, und Apple prüft Datenschutz-URL.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App-Datenschutz-Fragebogen (in App Store Connect, Kapitel „App-Datenschutz")
|
||||||
|
|
||||||
|
Das deckt sich exakt mit dem PrivacyInfo.xcprivacy. Antworten:
|
||||||
|
|
||||||
|
1. **„Erfasst die App Daten?"** — Ja
|
||||||
|
2. **Welche?**
|
||||||
|
- **Kontaktinformationen** → E-Mail-Adresse, Name
|
||||||
|
- **Standort** → Genauer Standort + Ungefährer Standort
|
||||||
|
- **Benutzerinhalt** → Fotos oder Videos, Sonstiger Benutzerinhalt (Tagebuch, Ausgaben-Notizen)
|
||||||
|
- **Bezeichner** → Benutzer-ID (banyaro-Account-ID)
|
||||||
|
- **Sonstige Daten** → Gesundheits- und Fitnessdaten (Workouts via HealthKit)
|
||||||
|
3. **Verwendung jeweils:** „App-Funktion"
|
||||||
|
4. **Mit Benutzer verknüpft:** Ja, bei allen (du hast einen Account)
|
||||||
|
5. **Wird für Tracking verwendet:** Nein, bei keinem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reviewer-Notiz (max. ~4000 Zeichen, freier Text)
|
||||||
|
|
||||||
|
```
|
||||||
|
Hallo Apple-Review-Team,
|
||||||
|
|
||||||
|
vielen Dank für die Prüfung von Ban Yaro Go.
|
||||||
|
|
||||||
|
Ban Yaro Go ist die native iOS-Ergänzung zur kostenlosen Community-
|
||||||
|
Plattform banyaro.app für Hundefreund:innen im deutschsprachigen Raum.
|
||||||
|
|
||||||
|
— DEMO-LOGIN —
|
||||||
|
Die App erfordert ein banyaro-Konto. Bitte nutzt zum Testen:
|
||||||
|
|
||||||
|
E-Mail: <reviewer@banyaro.app>
|
||||||
|
Passwort: <BITTE VOR EINREICHUNG SETZEN>
|
||||||
|
|
||||||
|
Der Account enthält zwei Beispielhunde, einige Touren, Tagebuch-
|
||||||
|
Einträge und Ausgaben. Sämtliche Server-Daten gehören diesem Demo-
|
||||||
|
Account und werden nach Review-Abschluss auf Wunsch gelöscht.
|
||||||
|
|
||||||
|
— HINTERGRUND-STANDORT (UIBackgroundModes location) —
|
||||||
|
Ban Yaro Go zeichnet auf Wunsch des Nutzers Gassi-Touren als
|
||||||
|
Polyline-Track auf. Während einer aktiven Tour läuft die App im
|
||||||
|
Hintergrund, damit der Track auch bei gesperrtem Display oder in der
|
||||||
|
Hosentasche weitergeführt wird. Zur Sichtbarmachung läuft parallel
|
||||||
|
eine Live-Aktivität in der Dynamic Island. Außerhalb einer aktiven
|
||||||
|
Tour wird kein Standort im Hintergrund erfasst.
|
||||||
|
|
||||||
|
Test der Funktion: Anmelden → Tab „Touren" → „Neue Tour", die
|
||||||
|
Aufnahme starten und das iPhone sperren — die Polyline wächst weiter,
|
||||||
|
die Live-Aktivität zeigt die laufende Strecke.
|
||||||
|
|
||||||
|
— HEALTHKIT (NSHealthUpdateUsageDescription) —
|
||||||
|
Ban Yaro Go schreibt ausschließlich (toShare:) Workouts vom Typ
|
||||||
|
„Walking" mit zugehöriger Route in Apple Health. Es werden keine
|
||||||
|
Health-Daten gelesen (read: []). Die NSHealthShareUsageDescription
|
||||||
|
existiert nur deshalb, weil iOS sie für jede HealthKit-Integration
|
||||||
|
abfragt — die App liest selbst nichts.
|
||||||
|
|
||||||
|
Aktivierung in der App: Mehr → Aufnahme → „Apple Health Sync".
|
||||||
|
|
||||||
|
— WEATHERKIT (Entitlement aktiv für app.banyaro.ios) —
|
||||||
|
Tab „Wetter" nutzt WeatherKit für hundefreundliche
|
||||||
|
Vorhersagen (Asphalttemperatur, UV, Zecken-Hinweis,
|
||||||
|
Niederschlag pro Stunde). Es werden keine Wetterdaten an Dritte
|
||||||
|
weitergegeben.
|
||||||
|
|
||||||
|
— FOTOS / KAMERA —
|
||||||
|
Fotos werden während einer Tour zur Punktmarkierung erzeugt
|
||||||
|
(Geolocation des Aufnahmezeitpunkts) und mit dem Tagebuch des
|
||||||
|
ausgewählten Hundes verknüpft. Es werden keine Fotos automatisch
|
||||||
|
hochgeladen — nur die, die der Nutzer aktiv mit „Sichern" bestätigt.
|
||||||
|
|
||||||
|
— GPX-IMPORT —
|
||||||
|
Andere Apps (z. B. Komoot, Files.app) können GPX-Tracks per
|
||||||
|
Teilen-Menü an Ban Yaro Go schicken. Die App registriert sich für
|
||||||
|
die UTI com.topografix.gpx als „Alternate Handler" (sie versucht
|
||||||
|
nicht, Standard-App für GPX zu werden). Der Nutzer bekommt einen
|
||||||
|
Bestätigungs-Dialog und kann den Track als Tour übernehmen oder nur
|
||||||
|
in Apple Maps öffnen.
|
||||||
|
|
||||||
|
— KONTO-LÖSCHUNG —
|
||||||
|
Mehr → „Konto löschen" entfernt das banyaro-Konto vollständig
|
||||||
|
(DELETE /api/profile/account, zweistufige Bestätigung). Damit wird
|
||||||
|
die Verpflichtung aus iOS-16.4-Guidelines erfüllt.
|
||||||
|
|
||||||
|
— PRIVATSPHÄRE —
|
||||||
|
Es findet kein Tracking im Sinne der App-Tracking-Transparency statt
|
||||||
|
(NSPrivacyTracking=false). Es werden keine IDFA, keine Drittanbieter-
|
||||||
|
Analytik und keine Werbe-SDKs eingebunden. Datenschutz-Richtlinie:
|
||||||
|
https://banyaro.app/datenschutz
|
||||||
|
|
||||||
|
Bei Fragen erreicht ihr uns unter mail@motocamp.de.
|
||||||
|
|
||||||
|
Vielen Dank und freundliche Grüße,
|
||||||
|
das banyaro-Team
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Mit Demo-Login ergänzen, bevor du auf „Submit" klickst.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versionsinformationen
|
||||||
|
|
||||||
|
- **Versionsnummer:** 1.0
|
||||||
|
- **Build-Nummer:** 1
|
||||||
|
- **Mindest-iOS:** 17.0
|
||||||
|
- **iPad-Support:** Nein (aktuell nur iPhone, Layout darauf optimiert)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test-Account Setup (für dich, bevor du einreichst)
|
||||||
|
|
||||||
|
1. In banyaro.app registrieren als `reviewer@banyaro.app` (oder ähnlich)
|
||||||
|
2. 2 Test-Hunde anlegen (z. B. „Luna, Border Collie" + „Max, Mischling")
|
||||||
|
3. 1–2 Touren vor dem Hochladen aufzeichnen (oder GPX importieren)
|
||||||
|
4. 1–2 Tagebuch-Einträge
|
||||||
|
5. 1 Gassi-Treffen für nächste Woche planen
|
||||||
|
6. Login-Daten in obige Reviewer-Notiz eintragen
|
||||||
|
|
||||||
|
Apple kann sonst die Hauptfunktion nicht testen → Rejection.
|
||||||
BIN
AppStore/screenshots/01_heim.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
AppStore/screenshots/02_touren_liste.png
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
AppStore/screenshots/03_tour_detail.png
Normal file
|
After Width: | Height: | Size: 914 KiB |
BIN
AppStore/screenshots/04_wetter.png
Normal file
|
After Width: | Height: | Size: 456 KiB |
BIN
AppStore/screenshots/05_gassi.png
Normal file
|
After Width: | Height: | Size: 556 KiB |
BIN
AppStore/screenshots/06_giftkoeder.png
Normal file
|
After Width: | Height: | Size: 587 KiB |
BIN
AppStore/screenshots/07_tagebuch.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
AppStore/screenshots/08_mehr.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
203
BanYaroGo/Views/DiaryDetailView.swift
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
import SwiftUI
|
||||||
|
import MapKit
|
||||||
|
import CoreLocation
|
||||||
|
|
||||||
|
struct DiaryDetailView: View {
|
||||||
|
let dogId: Int
|
||||||
|
@State var entry: DiaryEntry
|
||||||
|
let onChange: () async -> Void
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var photoIndex = 0
|
||||||
|
@State private var showDeleteConfirm = false
|
||||||
|
@State private var isDeleting = false
|
||||||
|
@State private var errorMessage: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
header
|
||||||
|
if let media = entry.mediaItems, !media.isEmpty {
|
||||||
|
gallery(media)
|
||||||
|
}
|
||||||
|
if let text = entry.text, !text.isEmpty {
|
||||||
|
Text(text)
|
||||||
|
.font(.body)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
}
|
||||||
|
if let tags = entry.tags, !tags.isEmpty {
|
||||||
|
tagsRow(tags)
|
||||||
|
}
|
||||||
|
if let lat = entry.gpsLat, let lon = entry.gpsLon {
|
||||||
|
locationCard(lat: lat, lon: lon)
|
||||||
|
}
|
||||||
|
deleteButton
|
||||||
|
if let errorMessage {
|
||||||
|
Text(errorMessage).font(.footnote).foregroundStyle(.red).padding(.horizontal, 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
.navigationTitle(entry.titel?.isEmpty == false ? entry.titel! : "Eintrag")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.alert("Eintrag löschen?", isPresented: $showDeleteConfirm) {
|
||||||
|
Button("Abbrechen", role: .cancel) {}
|
||||||
|
Button("Löschen", role: .destructive) { Task { await delete() } }
|
||||||
|
} message: {
|
||||||
|
Text("Dieser Tagebucheintrag und alle zugehörigen Fotos werden endgültig entfernt.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Header
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
if entry.isMilestoneFlag || entry.typ == "meilenstein" {
|
||||||
|
Label("Meilenstein", systemImage: "star.fill")
|
||||||
|
.font(.caption.bold())
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.orange.opacity(0.2), in: Capsule())
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
if let typ = entry.typ, !typ.isEmpty, typ != "meilenstein" {
|
||||||
|
Text(typ.capitalized)
|
||||||
|
.font(.caption.bold())
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.accentColor.opacity(0.15), in: Capsule())
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
if let datum = entry.datum {
|
||||||
|
Text(DiaryUtil.format(datum))
|
||||||
|
.font(.subheadline.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let loc = entry.locationName, !loc.isEmpty {
|
||||||
|
Label(loc, systemImage: "mappin.and.ellipse")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Photo gallery
|
||||||
|
|
||||||
|
private func gallery(_ media: [DiaryMedia]) -> some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
TabView(selection: $photoIndex) {
|
||||||
|
ForEach(Array(media.enumerated()), id: \.element.id) { idx, m in
|
||||||
|
AsyncImage(url: URL(string: "https://banyaro.app\(m.url)")) { phase in
|
||||||
|
switch phase {
|
||||||
|
case .success(let img): img.resizable().scaledToFit()
|
||||||
|
case .failure: Image(systemName: "photo")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
default: ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.tag(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: media.count > 1 ? .always : .never))
|
||||||
|
.frame(height: 320)
|
||||||
|
.background(Color.black.opacity(0.04))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
|
||||||
|
if media.count > 1 {
|
||||||
|
Text("\(photoIndex + 1) / \(media.count)")
|
||||||
|
.font(.caption.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tags
|
||||||
|
|
||||||
|
private func tagsRow(_ tags: [String]) -> some View {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
ForEach(tags, id: \.self) { tag in
|
||||||
|
Text("#" + tag)
|
||||||
|
.font(.caption.bold())
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.background(Color.secondary.opacity(0.15), in: Capsule())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Location
|
||||||
|
|
||||||
|
private func locationCard(lat: Double, lon: Double) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Label("Ort", systemImage: "location.fill")
|
||||||
|
.font(.caption.bold())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
|
||||||
|
Map(initialPosition: .region(MKCoordinateRegion(
|
||||||
|
center: CLLocationCoordinate2D(latitude: lat, longitude: lon),
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||||
|
))) {
|
||||||
|
Annotation(entry.titel ?? "Eintrag",
|
||||||
|
coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)) {
|
||||||
|
Image(systemName: "pawprint.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(.white, Color.accentColor)
|
||||||
|
.background(.white, in: Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 180)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.onTapGesture { openInMaps(lat: lat, lon: lon) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openInMaps(lat: Double, lon: Double) {
|
||||||
|
let item = MKMapItem(placemark: MKPlacemark(
|
||||||
|
coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)))
|
||||||
|
item.name = entry.titel ?? "Tagebuch-Eintrag"
|
||||||
|
item.openInMaps(launchOptions: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete
|
||||||
|
|
||||||
|
private var deleteButton: some View {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
showDeleteConfirm = true
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
if isDeleting { ProgressView() }
|
||||||
|
Image(systemName: "trash")
|
||||||
|
Text("Eintrag löschen").bold()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 44)
|
||||||
|
}
|
||||||
|
.disabled(isDeleting)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func delete() async {
|
||||||
|
isDeleting = true
|
||||||
|
errorMessage = nil
|
||||||
|
defer { isDeleting = false }
|
||||||
|
do {
|
||||||
|
try await APIClient.shared.delete("/api/dogs/\(dogId)/diary/\(entry.id)")
|
||||||
|
await onChange()
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,9 +50,15 @@ struct TagebuchView: View {
|
||||||
systemImage: "book",
|
systemImage: "book",
|
||||||
description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.")
|
description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.")
|
||||||
)
|
)
|
||||||
} else {
|
} else if let dog = activeDog.activeDog {
|
||||||
List(entries) { entry in
|
List(entries) { entry in
|
||||||
DiaryRow(entry: entry)
|
NavigationLink {
|
||||||
|
DiaryDetailView(dogId: dog.id, entry: entry) {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
DiaryRow(entry: entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
|
||||||