moodlit-native/Sources/Features/Sequences/SequenceListView.swift
till 9fe686780d feat: Favorit-Toggle + SequenceRow-Kontextmenü
MoodCard-Kontextmenü um Favorit/Aus-Favoriten erweitert (store.
toggleFavorite), an beiden Grid-Aufrufstellen verdrahtet. SequenceRow
bekommt ein Kontextmenü (Abspielen + Löschen) parallel zum Swipe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:24:16 +02:00

167 lines
4.4 KiB
Swift

import ManaAuthUI
import ManaCore
import SwiftUI
/// Liste der Sequenzen + Play-Button. **Pure-Native-Vorteil**:
/// Sequenz-Playback ist in der Web-App nicht implementiert (Store-
/// Methoden gab's, UI war nie verkabelt). Hier liefern wir's nativ.
public struct SequenceListView: View {
@Environment(MoodStore.self) private var store
@Environment(AuthClient.self) private var auth
@Environment(ManaAuthGate.self) private var gate
@State private var playing: PlayingSequence?
@State private var isCreating = false
public init() {}
public var body: some View {
List {
if store.sequences.isEmpty {
emptyState
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
} else {
ForEach(store.sequences) { seq in
SequenceRow(
sequence: seq,
moodLookup: store.moodById,
onPlay: { play(seq) },
onDelete: {
Task { try? await store.deleteSequence(id: seq.id) }
}
)
}
}
}
.listStyle(.plain)
.background(MoodlitTheme.background.ignoresSafeArea())
.navigationTitle("Sequenzen")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
gate.require(reason: "create-sequence") {
isCreating = true
}
} label: {
Label("Neu", systemImage: "plus")
}
}
}
.sheet(isPresented: $isCreating) {
CreateSequenceSheet { name, moodIds, durationSec in
Task {
do {
try await store.createSequence(name: name, moodIds: moodIds, durationSec: durationSec)
isCreating = false
} catch {}
}
}
}
.fullScreenCoverIfAvailable(item: $playing) { p in
SequencePlayerView(
sequence: p.sequence,
moods: p.moods,
onClose: { playing = nil }
)
}
.task { await store.loadAll() }
}
private var emptyState: some View {
VStack(spacing: 12) {
Image(systemName: "list.triangle")
.font(.system(size: 40))
.foregroundStyle(MoodlitTheme.mutedForeground)
Text("Noch keine Sequenzen")
.font(.body.weight(.medium))
Text("Verkettete Mood-Abfolgen mit Wechsel-Zeit. Wähle 2 oder mehr Moods.")
.font(.caption)
.foregroundStyle(MoodlitTheme.mutedForeground)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
}
private func play(_ sequence: MoodSequence) {
let moods = sequence.moodIds.compactMap(store.moodById)
guard !moods.isEmpty else { return }
playing = PlayingSequence(sequence: sequence, moods: moods)
}
}
private struct PlayingSequence: Identifiable {
let sequence: MoodSequence
let moods: [Mood]
var id: String { sequence.id }
}
private struct SequenceRow: View {
let sequence: MoodSequence
let moodLookup: (String) -> Mood?
let onPlay: () -> Void
let onDelete: () -> Void
var body: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 6) {
Text(sequence.name)
.font(.headline)
HStack(spacing: 6) {
ForEach(sequence.moodIds.prefix(6), id: \.self) { id in
Text(moodLookup(id)?.name ?? "?")
.font(.caption2.weight(.medium))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(MoodlitTheme.primary.opacity(0.20), in: Capsule())
.foregroundStyle(MoodlitTheme.primary)
}
if sequence.moodIds.count > 6 {
Text("+\(sequence.moodIds.count - 6)")
.font(.caption2)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
}
Text("\(sequence.durationSec)s je Mood · \(sequence.transitionSec)s Übergang")
.font(.caption2)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
Spacer()
Button(action: onPlay) {
Image(systemName: "play.circle.fill")
.font(.system(size: 32))
.foregroundStyle(MoodlitTheme.primary)
}
.buttonStyle(.plain)
}
.padding(.vertical, 8)
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: onDelete) {
Label("Löschen", systemImage: "trash")
}
}
.contextMenu {
Button(action: onPlay) {
Label("Abspielen", systemImage: "play.fill")
}
Divider()
Button(role: .destructive, action: onDelete) {
Label("Löschen", systemImage: "trash")
}
}
}
}
private extension View {
@ViewBuilder
func fullScreenCoverIfAvailable<Item: Identifiable, Content: View>(
item: Binding<Item?>,
@ViewBuilder content: @escaping (Item) -> Content
) -> some View {
#if os(iOS)
self.fullScreenCover(item: item, content: content)
#else
self.sheet(item: item, content: content)
#endif
}
}