diff --git a/AppStore/marketing.md b/AppStore/marketing.md new file mode 100644 index 0000000..4c91d96 --- /dev/null +++ b/AppStore/marketing.md @@ -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: +Passwort: + +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. diff --git a/AppStore/screenshots/01_heim.png b/AppStore/screenshots/01_heim.png new file mode 100644 index 0000000..7564e54 Binary files /dev/null and b/AppStore/screenshots/01_heim.png differ diff --git a/AppStore/screenshots/02_touren_liste.png b/AppStore/screenshots/02_touren_liste.png new file mode 100644 index 0000000..d67f78c Binary files /dev/null and b/AppStore/screenshots/02_touren_liste.png differ diff --git a/AppStore/screenshots/03_tour_detail.png b/AppStore/screenshots/03_tour_detail.png new file mode 100644 index 0000000..ddf20e7 Binary files /dev/null and b/AppStore/screenshots/03_tour_detail.png differ diff --git a/AppStore/screenshots/04_wetter.png b/AppStore/screenshots/04_wetter.png new file mode 100644 index 0000000..e864e1e Binary files /dev/null and b/AppStore/screenshots/04_wetter.png differ diff --git a/AppStore/screenshots/05_gassi.png b/AppStore/screenshots/05_gassi.png new file mode 100644 index 0000000..2a68328 Binary files /dev/null and b/AppStore/screenshots/05_gassi.png differ diff --git a/AppStore/screenshots/06_giftkoeder.png b/AppStore/screenshots/06_giftkoeder.png new file mode 100644 index 0000000..cecb3ce Binary files /dev/null and b/AppStore/screenshots/06_giftkoeder.png differ diff --git a/AppStore/screenshots/07_tagebuch.png b/AppStore/screenshots/07_tagebuch.png new file mode 100644 index 0000000..f736fff Binary files /dev/null and b/AppStore/screenshots/07_tagebuch.png differ diff --git a/AppStore/screenshots/08_mehr.png b/AppStore/screenshots/08_mehr.png new file mode 100644 index 0000000..430bace Binary files /dev/null and b/AppStore/screenshots/08_mehr.png differ diff --git a/BanYaroGo/Views/DiaryDetailView.swift b/BanYaroGo/Views/DiaryDetailView.swift new file mode 100644 index 0000000..35b2714 --- /dev/null +++ b/BanYaroGo/Views/DiaryDetailView.swift @@ -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 + } + } +} diff --git a/BanYaroGo/Views/TagebuchView.swift b/BanYaroGo/Views/TagebuchView.swift index 48cba9d..ab8138e 100644 --- a/BanYaroGo/Views/TagebuchView.swift +++ b/BanYaroGo/Views/TagebuchView.swift @@ -50,9 +50,15 @@ struct TagebuchView: View { systemImage: "book", description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.") ) - } else { + } else if let dog = activeDog.activeDog { List(entries) { entry in - DiaryRow(entry: entry) + NavigationLink { + DiaryDetailView(dogId: dog.id, entry: entry) { + await load() + } + } label: { + DiaryRow(entry: entry) + } } .listStyle(.plain) }