import Foundation import Observation struct HistoryEntry: Codable, Identifiable { let id: UUID let timestamp: Date let itemId: String let episodeId: String? let itemTitle: String let itemAuthor: String let chapterTitle: String? let position: Double } @Observable @MainActor final class HistoryManager { private(set) var entries: [HistoryEntry] = [] private static let maxEntries = 200 private var saveFile: URL { AppPaths.supportDirectory.appendingPathComponent("history.json") } init() { load() } func record(item: LibraryItem, position: Double, chapters: [Chapter]) { guard position >= 0 else { return } if let last = entries.first, last.itemId == item.id, last.episodeId == item.episodeId, abs(last.position - position) < 15 { return } let chapter = chapters.last { $0.start <= position } let entry = HistoryEntry( id: UUID(), timestamp: Date(), itemId: item.id, episodeId: item.episodeId, itemTitle: item.title, itemAuthor: item.author, chapterTitle: chapter?.title, position: position ) entries.insert(entry, at: 0) if entries.count > Self.maxEntries { entries.removeLast() } save() } func clear() { entries.removeAll() save() } func exportXML() -> URL? { let formatter = ISO8601DateFormatter() var xml = "\n" xml += "\n" for entry in entries { xml += " \n" xml += " \(formatter.string(from: entry.timestamp))\n" xml += " \(xmlEscape(entry.itemTitle))\n" xml += " \(xmlEscape(entry.itemAuthor))\n" if let chapter = entry.chapterTitle { xml += " \(xmlEscape(chapter))\n" } if let epId = entry.episodeId { xml += " \(xmlEscape(epId))\n" } xml += " \(entry.position)\n" xml += " \n" } xml += "\n" let tmpURL = FileManager.default.temporaryDirectory .appendingPathComponent("hoerverlauf.xml") try? xml.write(to: tmpURL, atomically: true, encoding: .utf8) return tmpURL } private func xmlEscape(_ s: String) -> String { s.replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") } private func save() { try? FileManager.default.createDirectory( at: AppPaths.supportDirectory, withIntermediateDirectories: true) if let data = try? JSONEncoder().encode(entries) { try? data.write(to: saveFile) } } private func load() { guard let data = try? Data(contentsOf: saveFile) else { return } entries = (try? JSONDecoder().decode([HistoryEntry].self, from: data)) ?? [] } }