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:
97
ABS Client/Audiobookshelf swift/Views/FullHistoryView.swift
Normal file
97
ABS Client/Audiobookshelf swift/Views/FullHistoryView.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FullHistoryView: View {
|
||||
@Environment(AppState.self) private var app
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(app.history.entries) { entry in
|
||||
let isCurrent = entry.itemId == app.currentItem?.id &&
|
||||
entry.episodeId == app.currentItem?.episodeId
|
||||
Button {
|
||||
dismiss()
|
||||
Task { await app.playFromHistory(entry) }
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(entry.itemTitle)
|
||||
.font(.subheadline.bold())
|
||||
HStack(spacing: 4) {
|
||||
if let ch = entry.chapterTitle {
|
||||
Text(ch).font(.caption).foregroundStyle(.secondary)
|
||||
Text("·").font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Text(formatTime(entry.position))
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(relativeTime(entry.timestamp))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
Spacer()
|
||||
if !isCurrent {
|
||||
Text(String(localized: "history.other_item"))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
if !app.history.entries.isEmpty {
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
app.history.clear()
|
||||
} label: {
|
||||
Text(String(localized: "history.clear"))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(String(localized: "history.title"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(String(localized: "settings.done")) { dismiss() }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.overlay {
|
||||
if app.history.entries.isEmpty {
|
||||
ContentUnavailableView(
|
||||
String(localized: "history.empty"),
|
||||
systemImage: "clock.arrow.circlepath",
|
||||
description: Text(String(localized: "history.empty_desc"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
#endif
|
||||
}
|
||||
|
||||
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 relativeTime(_ date: Date) -> String {
|
||||
let diff = Int(-date.timeIntervalSinceNow)
|
||||
if diff < 60 { return String(localized: "history.just_now") }
|
||||
if diff < 3600 { return String(format: String(localized: "history.minutes_ago"), diff / 60) }
|
||||
if diff < 86400 { return String(format: String(localized: "history.hours_ago"), diff / 3600) }
|
||||
return String(format: String(localized: "history.days_ago"), diff / 86400)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user