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:
Scarriffle
2026-05-25 18:40:52 +02:00
parent 15d8e71d09
commit fa47cae664
21 changed files with 1751 additions and 119 deletions

View File

@@ -12,13 +12,14 @@ enum LibraryLayout: String, CaseIterable, Identifiable {
struct LibraryListView: View {
let items: [LibraryItem]
var onRefresh: (() async -> Void)? = nil
var dimDownloading: Bool = false
let onSelect: (LibraryItem) -> Void
var body: some View {
#if os(iOS)
List {
ForEach(items) { item in
LibraryListRow(item: item)
LibraryListRow(item: item, dimDownloading: dimDownloading)
.contentShape(Rectangle())
.onTapGesture { onSelect(item) }
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
@@ -32,7 +33,7 @@ struct LibraryListView: View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(Array(items.enumerated()), id: \.element.id) { idx, item in
LibraryListRow(item: item)
LibraryListRow(item: item, dimDownloading: dimDownloading)
.contentShape(Rectangle())
.onTapGesture { onSelect(item) }
if idx < items.count - 1 {
@@ -49,10 +50,17 @@ struct LibraryListView: View {
struct LibraryListRow: View {
@Environment(AppState.self) private var app
let item: LibraryItem
var dimDownloading: Bool = false
var body: some View {
HStack(spacing: 12) {
cover
ZStack {
cover
.opacity(dimDownloading && isActivelyDownloading ? 0.55 : 1.0)
if dimDownloading, case .downloading(let p) = app.downloads.state(for: item.downloadKey) {
LargeDownloadOverlay(progress: p, size: 40)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.font(.headline)
@@ -77,18 +85,27 @@ struct LibraryListRow: View {
#endif
}
}
.opacity(dimDownloading && isActivelyDownloading ? 0.55 : 1.0)
Spacer(minLength: 8)
#if os(macOS)
if item.durationSeconds > 0 {
if dimDownloading, app.downloads.downloadedBytes(for: item.downloadKey) > 0 {
Text(formatBytes(app.downloads.downloadedBytes(for: item.downloadKey)))
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
.opacity(dimDownloading && isActivelyDownloading ? 0.55 : 1.0)
} else if item.durationSeconds > 0 {
Text(formatDuration(item.durationSeconds))
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
.opacity(dimDownloading && isActivelyDownloading ? 0.55 : 1.0)
}
#endif
downloadStatus
#if os(macOS)
.frame(width: 28)
#endif
if !(dimDownloading && isActivelyDownloading) {
downloadStatus
#if os(macOS)
.frame(width: 28)
#endif
}
}
#if os(macOS)
.padding(.horizontal, 16)
@@ -97,6 +114,11 @@ struct LibraryListRow: View {
.contextMenu { downloadMenuItems }
}
private var isActivelyDownloading: Bool {
if case .downloading = app.downloads.state(for: item.downloadKey) { return true }
return false
}
private var cover: some View {
Group {
if let url = app.client.coverURL(itemId: item.id) {