iOS 1.0 RC 2: Update Download and Update All Downloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-11-06 08:55:26 -08:00
1 parent abb157d140
commit ee6ca41fd3
4 files changed
+50 -11

No files matched your search

+1
View File
@@ -26,4 +26,5 @@
2026-11-06 iOS: Random Album Radio works like Plexamp. It shuffles the album list once, keeps about 8 albums queued ahead in that order, plays each album in track order, and tops up when 3 are left. The station survives a restart.
2026-11-06 iOS: Recently Added has See All, a full scrolling list sorted by date added. Tapping a song on the Library page plays from it through the last 500 added songs.
2026-11-06 iOS: stem playlist covers can come from Photos (Change Cover > Choose from Photos). Stored on the phone, 1200 px JPEG.
2026-11-06 iOS: Update Download on downloaded playlists, albums, artists and stem playlists (their menus and the Downloads tab), plus Update All Downloads. Pulls the current song list, downloads new songs, drops removed ones, retries failed ones. Downloads still sync on their own when the app opens.
2026-11-06 iOS: crossfade, Off or 1-12 s in Settings > Listening. Equal-power fade, Sweet Fades skips it between songs of the same album. Seeking during a fade finishes it.
+16 -2
View File
@@ -141,6 +141,20 @@ final class Downloads: @unchecked Sendable {
/// Brings every group in line with the server: playlists gain and lose songs, artists gain albums.
@MainActor func refresh() async {
for gr in groups.values {
if let tracks = await serverTracks(gr), let cur = groups[gr.id], cur.trackKeys != tracks.map(\.ratingKey) { download(cur, tracks) }
}
}
/// Update Download: brings [id] in line with the server now and retries its failed songs.
/// False when the server couldn't be reached.
@MainActor func update(_ id: String) async -> Bool {
guard let gr = groups[id], let tracks = await serverTracks(gr), let cur = groups[id] else { return false }
download(cur, tracks)
return true
}
/// The songs [gr] holds on the server right now, or nil when they can't be loaded.
@MainActor private func serverTracks(_ gr: DownloadGroup) async -> [Track]? {
let tracks: [Track]?
switch gr.kind {
case .playlist: tracks = try? await plex.playlistTracks(gr.key)
@@ -148,8 +162,8 @@ final class Downloads: @unchecked Sendable {
case .artist: tracks = try? await plex.artistTracks(gr.key)
case .stemPlaylist: tracks = AppGraph.shared.stemPlaylists.lists.first { $0.id == gr.key }?.tracks
}
if let tracks, !tracks.isEmpty, let cur = groups[gr.id], cur.trackKeys != tracks.map(\.ratingKey) { download(cur, tracks) }
}
guard let tracks, !tracks.isEmpty else { return nil }
return tracks
}
private func saveGroups() {
+18 -9
View File
@@ -71,8 +71,8 @@ struct MediaMenu {
g.playback.addToQueue(pl.tracks, mixes: pl.mixes); overlays.hud("Added to Queue", "text.line.last.and.arrowtriangle.forward")
},
]
a.append(downloadAction(pl.title, DownloadGroup(kind: .stemPlaylist, key: pl.id, title: pl.title, subtitle: "Stem Playlist", thumb: pl.thumb),
{ pl.tracks }, confirm: false))
a += downloadActions(pl.title, DownloadGroup(kind: .stemPlaylist, key: pl.id, title: pl.title, subtitle: "Stem Playlist", thumb: pl.thumb),
{ pl.tracks }, confirm: false)
if pl.items.isEmpty { a = [] }
let lists = g.stemPlaylists
if !pl.items.isEmpty {
@@ -134,7 +134,7 @@ struct MediaMenu {
private func collection(_ title: String, _ group: DownloadGroup?, _ load: @escaping () async throws -> [Track],
playlistKey: String?, extra: [SheetAction], confirmDownload: Bool = false, stemmed: Bool = false) -> [SheetAction] {
var a = queueActions(load)
if let group { a.append(downloadAction(title, group, load, confirm: confirmDownload)) }
if let group { a += downloadActions(title, group, load, confirm: confirmDownload) }
a.append(SheetAction("Add to Playlist…", icon: "text.badge.plus") { withTracks(load) { addToPlaylist($0, skip: playlistKey, title: title) } })
if !stemmed { a.append(stemNow(load)) }
return a + extra
@@ -152,14 +152,23 @@ struct MediaMenu {
]
}
/// Download, or Remove Download once [group] is downloaded. A downloaded group keeps itself in step with the server.
func downloadAction(_ title: String, _ group: DownloadGroup, _ load: @escaping () async throws -> [Track], confirm: Bool) -> SheetAction {
/// Download, or Update Download and Remove Download once [group] is downloaded. A downloaded group also
/// keeps itself in step with the server on its own; Update does it now and retries songs that failed.
func downloadActions(_ title: String, _ group: DownloadGroup, _ load: @escaping () async throws -> [Track], confirm: Bool) -> [SheetAction] {
if g.downloads.groups[group.id] != nil {
return SheetAction("Remove Download", icon: "trash", destructive: true) {
return [
SheetAction("Update Download", icon: "arrow.triangle.2.circlepath") {
withTracks(load) { tracks in
g.downloads.download(group, tracks)
overlays.hud("Download Updated", "arrow.triangle.2.circlepath")
}
},
SheetAction("Remove Download", icon: "trash", destructive: true) {
overlays.confirm("Remove Download?", "“\(title)” will no longer be available offline.", "Remove") { g.downloads.removeGroup(group.id) }
},
]
}
}
return SheetAction("Download", icon: "arrow.down.circle") {
return [SheetAction("Download", icon: "arrow.down.circle") {
withTracks(load) { tracks in
let go = { g.downloads.download(group, tracks); overlays.hud("Downloading", "arrow.down.circle") }
if confirm && tracks.count > 20 {
@@ -168,7 +177,7 @@ struct MediaMenu {
go()
}
}
}
}]
}
/// Has stemd on the server separate the songs that lack stems ahead of the rest of the library.
@@ -820,6 +820,12 @@ struct DownloadsScreen: View {
SheetAction("Shuffle", icon: "shuffle") { g.playback.playTracks(d.tracks, shuffle: true) },
]
if d.failed > 0 { items.append(SheetAction("Retry Failed Songs", icon: "arrow.clockwise") { g.downloads.retryFailed() }) }
items.append(SheetAction("Update Download", icon: "arrow.triangle.2.circlepath") {
Task {
let ok = await g.downloads.update(d.group.id)
ui.overlays.hud(ok ? "Download Updated" : "Couldn't reach the server", ok ? "arrow.triangle.2.circlepath" : "exclamationmark.triangle")
}
})
items.append(SheetAction("Remove Download", icon: "trash", destructive: true) {
ui.overlays.confirm("Remove Download?", "“\(d.group.title)” will no longer be available offline.", "Remove") { g.downloads.removeGroup(d.group.id) }
})
@@ -851,6 +857,15 @@ struct DownloadsScreen: View {
private func moreItems(_ all: [DownloadedGroup], _ pinned: String) -> [SheetAction] {
var a: [SheetAction] = []
if all.contains(where: { $0.failed > 0 }) { a.append(SheetAction("Retry Failed", icon: "arrow.clockwise") { g.downloads.retryFailed() }) }
if !g.downloads.groups.isEmpty {
a.append(SheetAction("Update All Downloads", icon: "arrow.triangle.2.circlepath") {
Task {
var failed = 0
for id in Array(g.downloads.groups.keys) { if await !g.downloads.update(id) { failed += 1 } }
ui.overlays.hud(failed == 0 ? "Downloads Updated" : "\(failed) couldn't be updated", failed == 0 ? "arrow.triangle.2.circlepath" : "exclamationmark.triangle")
}
})
}
a.append(SheetAction("Remove All Downloads", icon: "trash", destructive: true) {
ui.overlays.confirm("Remove All Downloads?", "Frees \(pinned) GB. Songs stay in your library.", "Remove All") {
for id in g.downloads.groups.keys { g.downloads.removeGroup(id) }