import SwiftUI struct PlaybackDetailsView: View { @Environment(AppState.self) private var app enum Tab: String, CaseIterable { case chapters = "Kapitel" case bookmarks = "Lesezeichen" } @State private var selectedTab: Tab = .chapters @State private var showAddBookmark: Bool = false @State private var newBookmarkTitle: String = "" var body: some View { NavigationStack { VStack(spacing: 0) { Picker(selection: $selectedTab) { ForEach(Tab.allCases, id: \.self) { tab in Text(tab.rawValue).tag(tab) } } label: { EmptyView() } .pickerStyle(.segmented) .labelsHidden() .padding(.horizontal) .padding(.top, 8) .padding(.bottom, 4) Divider() switch selectedTab { case .chapters: chaptersTab case .bookmarks: bookmarksTab } } .navigationTitle(selectedTab.rawValue) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } .alert("Lesezeichen hinzufügen", isPresented: $showAddBookmark) { TextField("Name", text: $newBookmarkTitle) Button("Hinzufügen") { let title = newBookmarkTitle.trimmingCharacters(in: .whitespaces) app.addBookmark(title: title.isEmpty ? defaultBookmarkName : title) newBookmarkTitle = "" } Button("Abbrechen", role: .cancel) { newBookmarkTitle = "" } } message: { Text("Gib einen Namen für dieses Lesezeichen ein.") } #if os(iOS) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) #else .frame(minWidth: 420, minHeight: 520) #endif } // MARK: - Chapters tab private var chaptersTab: some View { let chapters = app.currentItem?.chapters ?? [] let current = app.player.currentChapter return ScrollViewReader { proxy in List { if !chapters.isEmpty { Section { chapterNavigationBar } } ForEach(chapters) { chapter in Button { app.seekAbsolute(chapter.start) } label: { HStack(spacing: 10) { if chapter.id == current?.id { Image(systemName: "play.fill") .font(.caption) .foregroundStyle(.tint) .frame(width: 14) } else { Spacer().frame(width: 14) } VStack(alignment: .leading, spacing: 2) { Text(chapter.title) .font(chapter.id == current?.id ? .body.bold() : .body) .foregroundStyle(chapter.id == current?.id ? Color.accentColor : Color.primary) Text(formatTime(chapter.start)) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } Spacer() Text(formatDuration(chapter.end - chapter.start)) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } .contentShape(Rectangle()) } .buttonStyle(.plain) .id(chapter.id) } } .listStyle(.plain) .onAppear { guard let id = current?.id else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { withAnimation(.easeInOut(duration: 0.4)) { proxy.scrollTo(id, anchor: .center) } } } } } private var chapterNavigationBar: some View { let chapters = app.currentItem?.chapters ?? [] let current = app.player.currentChapter let currentIdx = chapters.firstIndex { $0.id == current?.id } let hasPrev = (currentIdx ?? 0) > 0 let hasNext = (currentIdx.map { $0 < chapters.count - 1 }) ?? false return HStack(spacing: 0) { Spacer() navButton(systemImage: "chevron.backward.to.line", help: "Kapitelanfang", disabled: current == nil) { if let ch = current { app.seekAbsolute(ch.start) } } Spacer() navButton(systemImage: "chevron.backward", help: "Vorheriges Kapitel", disabled: !hasPrev) { if let idx = currentIdx { app.seekAbsolute(chapters[idx - 1].start) } } Spacer() navButton(systemImage: "chevron.forward", help: "Nächstes Kapitel", disabled: !hasNext) { if let idx = currentIdx { app.seekAbsolute(chapters[idx + 1].start) } } Spacer() navButton(systemImage: "chevron.forward.to.line", help: "Kapitelende", disabled: current == nil) { if let ch = current { app.seekAbsolute(max(ch.start, ch.end - 0.5)) } } Spacer() } .padding(.vertical, 4) } private func navButton(systemImage: String, help: String, disabled: Bool, action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: systemImage) .font(.title3) .frame(minWidth: 44, minHeight: 36) } .buttonStyle(.plain) .disabled(disabled) .foregroundStyle(disabled ? Color.secondary : Color.accentColor) .help(help) } // MARK: - Bookmarks tab private var bookmarksTab: some View { let item = app.currentItem let itemBookmarks = item.map { app.bookmarks.bookmarks(for: $0) } ?? [] return List { Section { Button { newBookmarkTitle = defaultBookmarkName showAddBookmark = true } label: { Label("Lesezeichen hinzufügen", systemImage: "bookmark.fill") } .disabled(item == nil || !app.player.isReady) } ForEach(itemBookmarks) { bm in Button { app.seekAbsolute(bm.time) } label: { VStack(alignment: .leading, spacing: 3) { Text(bm.title) .font(.subheadline.bold()) HStack(spacing: 4) { if let ch = bm.chapterTitle { Text(ch).font(.caption).foregroundStyle(.secondary) Text("·").font(.caption).foregroundStyle(.secondary) } Text(formatTime(bm.time)) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } .contentShape(Rectangle()) } .buttonStyle(.plain) } .onDelete { offsets in for idx in offsets { app.deleteBookmark(itemBookmarks[idx]) } } } .listStyle(.plain) .overlay { if itemBookmarks.isEmpty { ContentUnavailableView( "Keine Lesezeichen", systemImage: "bookmark", description: Text(String(localized: "details.no_bookmarks_desc")) ) } } } // MARK: - Helpers private var defaultBookmarkName: String { let t = app.player.absoluteCurrentTime if let ch = app.player.currentChapter { return "\(ch.title) · \(formatTime(t))" } return formatTime(t) } private func formatTime(_ seconds: Double) -> String { guard seconds.isFinite, seconds >= 0 else { return "0:00" } let total = Int(seconds) let h = total / 3600, m = (total % 3600) / 60, s = total % 60 return h > 0 ? String(format: "%d:%02d:%02d", h, m, s) : String(format: "%d:%02d", m, s) } private func formatDuration(_ seconds: Double) -> String { guard seconds.isFinite, seconds > 0 else { return "" } let total = Int(seconds) let h = total / 3600, m = (total % 3600) / 60, s = total % 60 if h > 0 { return String(format: "%dh %02dm", h, m) } if m > 0 { return String(format: "%dm %02ds", m, s) } return String(format: "%ds", s) } }