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:
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
struct Bookmark: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let createdAt: Date
|
||||
let itemId: String
|
||||
let episodeId: String?
|
||||
let itemTitle: String
|
||||
var title: String
|
||||
let chapterTitle: String?
|
||||
let time: Double
|
||||
}
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class BookmarkManager {
|
||||
private(set) var bookmarks: [Bookmark] = []
|
||||
|
||||
private var saveFile: URL {
|
||||
AppPaths.supportDirectory.appendingPathComponent("bookmarks.json")
|
||||
}
|
||||
|
||||
init() { load() }
|
||||
|
||||
func add(item: LibraryItem, time: Double, title: String, chapters: [Chapter]) {
|
||||
let chapter = chapters.last { $0.start <= time }
|
||||
let bm = Bookmark(
|
||||
id: UUID(),
|
||||
createdAt: Date(),
|
||||
itemId: item.id,
|
||||
episodeId: item.episodeId,
|
||||
itemTitle: item.title,
|
||||
title: title,
|
||||
chapterTitle: chapter?.title,
|
||||
time: time
|
||||
)
|
||||
bookmarks.insert(bm, at: 0)
|
||||
save()
|
||||
}
|
||||
|
||||
func delete(_ bookmark: Bookmark) {
|
||||
bookmarks.removeAll { $0.id == bookmark.id }
|
||||
save()
|
||||
}
|
||||
|
||||
func bookmarks(for item: LibraryItem) -> [Bookmark] {
|
||||
bookmarks.filter { $0.itemId == item.id && $0.episodeId == item.episodeId }
|
||||
.sorted { $0.time < $1.time }
|
||||
}
|
||||
|
||||
/// Replaces bookmarks for an item with the server-loaded list (keeps bookmarks for other items).
|
||||
func mergeFromServer(_ serverBookmarks: [ServerBookmark], for item: LibraryItem) {
|
||||
bookmarks.removeAll { $0.itemId == item.id && $0.episodeId == item.episodeId }
|
||||
let chapters = item.chapters
|
||||
for sb in serverBookmarks {
|
||||
let chapter = chapters.last { $0.start <= sb.time }
|
||||
let bm = Bookmark(
|
||||
id: UUID(),
|
||||
createdAt: sb.createdAt.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date(),
|
||||
itemId: item.id,
|
||||
episodeId: item.episodeId,
|
||||
itemTitle: item.title,
|
||||
title: sb.title,
|
||||
chapterTitle: chapter?.title,
|
||||
time: sb.time
|
||||
)
|
||||
bookmarks.append(bm)
|
||||
}
|
||||
bookmarks.sort { $0.createdAt > $1.createdAt }
|
||||
save()
|
||||
}
|
||||
|
||||
private func save() {
|
||||
try? FileManager.default.createDirectory(
|
||||
at: AppPaths.supportDirectory, withIntermediateDirectories: true)
|
||||
if let data = try? JSONEncoder().encode(bookmarks) {
|
||||
try? data.write(to: saveFile)
|
||||
}
|
||||
}
|
||||
|
||||
private func load() {
|
||||
guard let data = try? Data(contentsOf: saveFile) else { return }
|
||||
bookmarks = (try? JSONDecoder().decode([Bookmark].self, from: data)) ?? []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user