iOS: stem playlist refresh and covers, download queue page, reorderable Library, whole-library radio

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Sonnet 5.5 committed 2026-10-28 15:49:09 -07:00
1 parent 7c8dfb54ca
commit a4c870251c
18 files changed
+514 -30

No files matched your search

+33 -5
View File
@@ -69,13 +69,14 @@ final class CarLibrary: @unchecked Sendable {
func children(_ id: String) async throws -> [CarItem]? {
let away = await away()
switch id {
case CarIds.downloads: return await songs(CarIds.downloads, try tracksOf(CarIds.downloads), shuffleRow: true, showArtist: true)
case CarIds.downloads: return downloadsMenu()
case CarIds.playlists: return try await playlists(away)
case CarIds.albums: return try await albumsTab(away)
case CarIds.artists: return try await artistsTab(away)
default:
guard let (kind, key) = CarIds.split(id) else { return nil }
switch kind {
case "dl": return try await downloadsView(key)
case "playlist", "stemplaylist": return await songs(id, try tracksOf(id), shuffleRow: true, showArtist: true)
case "album": return await songs(id, try tracksOf(id), shuffleRow: false, showArtist: false)
case "artist": return try await artistPage(key, away)
@@ -87,15 +88,42 @@ final class CarLibrary: @unchecked Sendable {
}
}
private func playlists(_ away: Bool) async throws -> [CarItem] {
/// Downloads opens onto the same categories as the other tabs, each limited to what's downloaded.
private func downloadsMenu() -> [CarItem] {
let dl = downloadedTracks()
guard !dl.isEmpty else { return [] }
return [
CarItem(id: "dl:playlists", title: "Playlists", art: .letter("P"), playable: false),
CarItem(id: "dl:albums", title: "Albums", subtitle: CarTree.countLabel(albumsOf(dl).count, "album", "albums"), art: .letter("A"), playable: false),
CarItem(id: "dl:artists", title: "Artists", subtitle: CarTree.countLabel(Set(dl.compactMap(\.artistKey)).count, "artist", "artists"), art: .letter("A"), playable: false),
CarItem(id: "dl:songs", title: "Songs", subtitle: CarTree.countLabel(dl.count, "song", "songs"), art: .letter("S"), playable: false),
]
}
private func downloadsView(_ key: String) async throws -> [CarItem]? {
switch key {
case "playlists": return try await playlists(true, onlyDownloaded: true)
case "albums": return try await albumsFor(true).map { albumItem($0) }
case "artists": return try await artistsFor(true).map { artistItem($0) }
case "songs": return await songs(CarIds.downloads, try tracksOf(CarIds.downloads), shuffleRow: true, showArtist: true)
default: return nil
}
}
private func playlists(_ away: Bool, onlyDownloaded: Bool = false) async throws -> [CarItem] {
let all = try await plex.playlists()
var shown: [Playlist] = []
if away {
if onlyDownloaded {
let have = Set(downloadedTracks().map(\.ratingKey))
for p in all where ((try? await plex.playlistTracks(p.ratingKey))?.contains { have.contains($0.ratingKey) } == true) { shown.append(p) }
} else if away {
for p in all where ((try? await tracksOf(CarIds.playlist(p.ratingKey)))?.isEmpty == false) { shown.append(p) }
} else {
shown = all
}
let stem = await MainActor.run { AppGraph.shared.stemPlaylists.lists.filter { !$0.items.isEmpty } }
let have = Set(downloadedTracks().map(\.ratingKey))
let stem = await MainActor.run { AppGraph.shared.stemPlaylists.lists.filter { l in
onlyDownloaded ? (l.tracks.contains { have.contains($0.ratingKey) }) : !l.items.isEmpty } }
let stemItems = stem.map { l in
CarItem(id: "stemplaylist:\(l.id)", title: l.title, subtitle: CarTree.countLabel(l.items.count, "song", "songs"),
art: artOr(l.thumb, l.title), playable: false, group: "Stem Playlists")
@@ -263,7 +291,7 @@ final class CarLibrary: @unchecked Sendable {
private func songItem(_ container: String, _ t: Track, _ showArtist: Bool, _ group: String?) -> CarItem {
// Stems are the norm, so only the exceptions are marked.
let noStems = stems.index.size > 0 && !pm.hasStems(t)
let sub = [noStems ? "No stems" : nil, showArtist ? t.artist : nil].compactMap { $0 }.joined(separator: " · ")
let sub = [noStems ? (stems.state(t) == .stemming ? "Stemming" : stems.state(t) == .queued ? "Queued for stems" : "No stems") : nil, showArtist ? t.artist : nil].compactMap { $0 }.joined(separator: " · ")
return CarItem(id: CarIds.play(container, t.ratingKey), title: t.title, subtitle: sub.isEmpty ? nil : sub,
art: t.thumb.map { .thumb($0) } ?? .letter(CarTree.letterOf(t.album)), playable: true, group: group,
downloaded: pm.playableOffline(t), trackKey: t.ratingKey)