Files
ABS-Client/ABS Client/Audiobookshelf swift/Models/Models.swift
Scarriffle a17a61ad9a Home screen, library-wide search, splash with orbital particles, macOS download fix
- Add Netflix-style Home with recent audiobooks/podcasts, list/grid toggle shared with Library
- Add searchable libraries — audiobooks filter on title/author; podcasts also index and search episodes across the whole library
- Add unified context menu (download / progress remove) across Home, Library, and PodcastDetail
- Rework cover badges: listened checkmark top-right, download indicator bottom-right; hide progress bar when finished
- Add AirPlay picker in PlayerBar
- Replace splash with four-phase animation: orbital particles → attract → flash → fade; cross-platform iOS+macOS, runtime AppIcon lookup
- Fix macOS parallel downloads: align connection settings with iOS, raise timeoutIntervalForRequest to 300s
- Fix progress deletion using server-side progress UUID (DELETE /api/me/progress/:id)
- Bump CFBundleVersion to 8

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 08:54:43 +02:00

99 lines
2.8 KiB
Swift

import Foundation
struct Chapter: Codable, Identifiable, Hashable {
let id: Int
let start: Double
let end: Double
let title: String
}
struct Library: Codable, Identifiable, Hashable {
let id: String
let name: String
let mediaType: String?
}
/// Wichtig für `ForEach`: Synthetische Podcast-Episoden-Items (z.B. aus der
/// library-weiten Folgensuche) teilen sich denselben `id` (Podcast-Container-ID)
/// und unterscheiden sich nur über `episodeId`. Default-`Identifiable` würde
/// hier kollidieren. **Immer** `ForEach(items, id: \.syncKey)` benutzen wenn
/// die Liste sowohl Container als auch Episoden-Items enthalten kann.
struct LibraryItem: Codable, Identifiable, Hashable {
let id: String
let title: String
let author: String
let durationSeconds: Double
let audioFiles: [AudioFile]
var mediaType: String = "book"
var episodeId: String? = nil
var description: String? = nil
var chapters: [Chapter] = []
var isPodcast: Bool { mediaType == "podcast" }
var isPodcastContainer: Bool { isPodcast && episodeId == nil }
var isPodcastEpisode: Bool { isPodcast && episodeId != nil }
static func == (lhs: LibraryItem, rhs: LibraryItem) -> Bool {
lhs.id == rhs.id && lhs.episodeId == rhs.episodeId
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(episodeId)
}
}
struct PodcastEpisode: Identifiable, Hashable, Codable {
let id: String
let title: String
let pubDate: String?
let publishedAtMillis: Double?
let season: String?
let episode: String?
let durationSeconds: Double
let audioFile: AudioFile
var formattedDate: String? {
if let ms = publishedAtMillis, ms > 0 {
let date = Date(timeIntervalSince1970: ms / 1000.0)
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .none
return df.string(from: date)
}
return pubDate
}
}
struct AudioFile: Codable, Hashable {
let ino: String
let filename: String
let ext: String
let durationSeconds: Double
let index: Int
}
struct PlaybackProgress: Codable, Hashable {
let itemId: String
var episodeId: String?
var currentTime: Double
var duration: Double
var isFinished: Bool
var updatedAt: Date
/// Server-assigned UUID for this progress entry. Required to delete it
/// Audiobookshelf's `DELETE /api/me/progress/:id` matches by this id, NOT
/// by libraryItemId. Optional so older persisted caches still decode.
var id: String?
var syncKey: String {
if let episodeId { return "\(itemId)|\(episodeId)" }
return itemId
}
}
enum DownloadState: Equatable {
case notDownloaded
case downloading(progress: Double)
case downloaded
case failed(message: String)
}