Files
ABS-Client/ABS Client/Audiobookshelf swift/Views/FullHistoryView.swift
Scarriffle fa47cae664 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>
2026-05-25 18:43:16 +02:00

98 lines
4.1 KiB
Swift

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)
}
}