Compare commits

...

2 commits

Author SHA1 Message Date
Till JS
710ede6acd fix(api): code:nil bei AuthError.serverError-Calls
Anpassung an die neue ManaCore-Signatur
`AuthError.serverError(status:code:message:)` (vorher ohne `code`).
Drei Call-Sites in fetchMedia, deleteMedia und ensureOK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:09:01 +02:00
Till JS
6805bd78c7 feat(decks): iOS-26 tabViewBottomAccessory für „Neues Deck"-Pille
Ersetzt den bottomBar-„+"-Button auf iOS 26 durch eine schwebende
Liquid-Glass-Pille via `.tabViewBottomAccessory`, nur sichtbar wenn
der Decks-Tab aktiv ist. iOS 18-Geräte behalten den bestehenden
bottomBar-Button (gated via `if #unavailable(iOS 26.0)`).

`showCreate` wandert als Binding von DeckListView nach RootView,
damit das Accessory den Sheet triggern kann.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:08:57 +02:00
3 changed files with 60 additions and 13 deletions

View file

@ -7,6 +7,7 @@ struct RootView: View {
@Environment(AuthClient.self) private var auth
@State private var selectedTab: AppTab = .decks
@State private var pendingDeepLinkSlug: String?
@State private var showCreateDeck = false
var body: some View {
Group {
@ -29,7 +30,7 @@ struct RootView: View {
@ViewBuilder
private var mainTabs: some View {
TabView(selection: $selectedTab) {
DeckListView()
DeckListView(showCreate: $showCreateDeck)
.tabItem { Label("Decks", systemImage: "rectangle.stack") }
.tag(AppTab.decks)
@ -43,6 +44,9 @@ struct RootView: View {
.tabItem { Label("Account", systemImage: "person.crop.circle") }
.tag(AppTab.account)
}
.decksCreateAccessory(visible: selectedTab == .decks) {
showCreateDeck = true
}
}
/// Universal-Link- und URL-Scheme-Handler:
@ -65,3 +69,38 @@ enum AppTab: Hashable {
case explore
case account
}
private extension View {
/// iOS 26: floating Neues Deck"-Pille via `.tabViewBottomAccessory`,
/// nur sichtbar wenn der Decks-Tab aktiv ist. iOS 18 fällt auf den
/// bestehenden `.bottomBar`-+"-Toolbar-Button in `DeckListView` zurück.
@ViewBuilder
func decksCreateAccessory(visible: Bool, onTap: @escaping () -> Void) -> some View {
if #available(iOS 26.0, *) {
self.tabViewBottomAccessory {
if visible {
DeckCreateAccessoryPill(action: onTap)
}
}
} else {
self
}
}
}
@available(iOS 26.0, *)
private struct DeckCreateAccessoryPill: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label("Neues Deck", systemImage: "plus")
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 8)
}
.buttonStyle(.borderedProminent)
.tint(CardsTheme.primary)
.accessibilityLabel("Neues Deck erstellen")
}
}

View file

@ -170,7 +170,7 @@ actor CardsAPI {
func fetchMedia(id: String) async throws -> Data {
let (data, http) = try await transport.request(path: "/api/v1/media/\(id)")
guard (200 ..< 300).contains(http.statusCode) else {
throw AuthError.serverError(status: http.statusCode, message: "media fetch failed")
throw AuthError.serverError(status: http.statusCode, code: nil, message: "media fetch failed")
}
return data
}
@ -178,7 +178,7 @@ actor CardsAPI {
/// `DELETE /api/v1/media/:id` Soft-Forget. (Endpoint heute nicht
/// implementiert serverseitig; Stub bleibt für späteren Use.)
func deleteMedia(id _: String) async throws {
throw AuthError.serverError(status: 501, message: "media delete not implemented on server")
throw AuthError.serverError(status: 501, code: nil, message: "media delete not implemented on server")
}
// MARK: - Deck-Mutations
@ -326,7 +326,7 @@ actor CardsAPI {
private func ensureOK(_ http: HTTPURLResponse, data: Data) throws {
guard (200 ..< 300).contains(http.statusCode) else {
let message = (try? JSONDecoder().decode(CardsServerError.self, from: data))?.error
throw AuthError.serverError(status: http.statusCode, message: message)
throw AuthError.serverError(status: http.statusCode, code: nil, message: message)
}
}
}

View file

@ -18,9 +18,10 @@ struct DeckListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \CachedDeck.updatedAt, order: .reverse) private var decks: [CachedDeck]
@Binding var showCreate: Bool
@State private var store: DeckListStore?
@State private var showAccount = false
@State private var showCreate = false
@State private var pendingShares: [PendingShare] = []
@State private var path = NavigationPath()
@ -235,7 +236,7 @@ struct DeckListView: View {
Label("Noch keine Decks", systemImage: "rectangle.stack")
.foregroundStyle(CardsTheme.foreground)
} description: {
Text("Tippe oben auf »+«, um dein erstes Deck zu erstellen, oder browse den Marketplace im Entdecken-Tab.")
Text("Tippe unten auf »+«, um dein erstes Deck zu erstellen, oder browse den Marketplace im Entdecken-Tab.")
.foregroundStyle(CardsTheme.mutedForeground)
}
}
@ -245,14 +246,21 @@ struct DeckListView: View {
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .topBarLeading) {
Button {
showCreate = true
} label: {
Image(systemName: "plus.circle")
.foregroundStyle(CardsTheme.primary)
// Auf iOS 26 übernimmt das `.tabViewBottomAccessory` aus RootView die
// Neues Deck"-Pille. Doppelten +"-Button im Liquid-Glass-Layout
// vermeiden bottomBar-Button nur auf iOS < 26 zeigen.
if #unavailable(iOS 26.0) {
ToolbarItemGroup(placement: .bottomBar) {
Button {
showCreate = true
} label: {
Label("Deck hinzufügen", systemImage: "plus")
.labelStyle(.iconOnly)
.foregroundStyle(CardsTheme.primary)
}
.accessibilityLabel("Deck hinzufügen")
Spacer()
}
.accessibilityLabel("Deck hinzufügen")
}
ToolbarItem(placement: .topBarTrailing) {
Button {