Add chapters, history, bookmarks, live download progress, and i18n
- Chapter navigation with auto-scroll to current chapter and end-of-chapter sleep timer - Opt-in listening history (local-only) with XML export and per-item quick menu - Bookmarks with server sync via Audiobookshelf API - Live MB counter during downloads via URLSessionDownloadTask delegate - In-progress downloads shown in "Heruntergeladen" with dimmed cover + ring overlay - Cover image cache (50 MB memory / 500 MB disk URLCache) - German/English localization (de.lproj, en.lproj) - Loading spinner now triggers immediately on view switch instead of waiting for the network Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,11 @@ import SwiftUI
|
||||
struct PlayerBar: View {
|
||||
@Environment(AppState.self) private var app
|
||||
@AppStorage("skipDurationSeconds") private var skipSeconds: Int = 30
|
||||
@AppStorage("historyEnabled") private var historyEnabled: Bool = false
|
||||
@State private var scrubbing: Bool = false
|
||||
@State private var scrubValue: Double = 0
|
||||
@State private var showDetails: Bool = false
|
||||
@State private var showFullHistory: Bool = false
|
||||
|
||||
var body: some View {
|
||||
if let item = app.currentItem {
|
||||
@@ -21,6 +24,14 @@ struct PlayerBar: View {
|
||||
.background(.bar)
|
||||
}
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
.sheet(isPresented: $showDetails) {
|
||||
PlaybackDetailsView()
|
||||
.environment(app)
|
||||
}
|
||||
.sheet(isPresented: $showFullHistory) {
|
||||
FullHistoryView()
|
||||
.environment(app)
|
||||
}
|
||||
} else if app.isPreparingPlayback {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
@@ -77,6 +88,17 @@ struct PlayerBar: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if historyEnabled {
|
||||
historyQuickMenu
|
||||
}
|
||||
|
||||
Button { showDetails = true } label: {
|
||||
Image(systemName: "list.bullet.indent")
|
||||
.font(.system(size: 22))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!app.player.isReady)
|
||||
|
||||
sleepMenu
|
||||
|
||||
rateMenu
|
||||
@@ -119,6 +141,17 @@ struct PlayerBar: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if historyEnabled {
|
||||
historyQuickMenu
|
||||
}
|
||||
|
||||
Button { showDetails = true } label: {
|
||||
Image(systemName: "list.bullet.indent")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!app.player.isReady)
|
||||
.help("Kapitel & Lesezeichen")
|
||||
|
||||
statusIndicator
|
||||
|
||||
Button {
|
||||
@@ -235,6 +268,51 @@ struct PlayerBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var detailsButtonVisible: Bool {
|
||||
app.player.isReady
|
||||
}
|
||||
|
||||
private var historyQuickMenu: some View {
|
||||
Menu {
|
||||
let recent = Array(app.history.entries
|
||||
.filter { $0.itemId == app.currentItem?.id && $0.episodeId == app.currentItem?.episodeId }
|
||||
.prefix(5))
|
||||
if recent.isEmpty {
|
||||
Text(String(localized: "history.empty"))
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(recent) { entry in
|
||||
Button {
|
||||
app.seekAbsolute(entry.position)
|
||||
} label: {
|
||||
let timeStr = formatTime(entry.position)
|
||||
let label = entry.chapterTitle.map { "\($0) · \(timeStr)" } ?? timeStr
|
||||
Label(label, systemImage: "clock")
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
Button {
|
||||
showFullHistory = true
|
||||
} label: {
|
||||
Label(String(localized: "player.history_all"), systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
#if os(iOS)
|
||||
.font(.system(size: 22))
|
||||
#else
|
||||
.font(.system(size: 16))
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.menuStyle(.borderlessButton)
|
||||
.fixedSize()
|
||||
#endif
|
||||
.menuIndicator(.hidden)
|
||||
.help(String(localized: "player.history_recent"))
|
||||
}
|
||||
|
||||
private var sleepMenu: some View {
|
||||
Menu {
|
||||
sleepOption(title: "Aus", mode: .off)
|
||||
@@ -244,6 +322,9 @@ struct PlayerBar: View {
|
||||
sleepOption(title: "30 Minuten", mode: .minutes(30))
|
||||
sleepOption(title: "1 Stunde", mode: .minutes(60))
|
||||
sleepOption(title: endOfPlaybackLabel, mode: .endOfBook)
|
||||
if !(app.currentItem?.chapters.isEmpty ?? true) {
|
||||
sleepOption(title: "Bis Ende des Kapitels", mode: .endOfChapter)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: app.player.sleepTimer == .off ? "moon.zzz" : "moon.zzz.fill")
|
||||
#if os(iOS)
|
||||
|
||||
Reference in New Issue
Block a user