- 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>
87 lines
2.6 KiB
Swift
87 lines
2.6 KiB
Swift
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)) ?? []
|
|
}
|
|
}
|