feat(android,ios): play from Downloads, and say what a download waits for

- A finished download row on the phones plays when tapped and has an
  Open button for its title, as the desktop's does.
- iOS queued downloads say "Waiting for Wi-Fi" or "Waiting for a
  connection", as Android's already do.
- Android download workers post their notifications as one group, each
  named for its episode, under a "Downloading 12 episodes" summary.
  Before, a season made fourteen loose notifications that all said
  "Making episode available offline".
This commit is contained in:
nvrl committed 2026-10-13 15:48:59 +02:00
1 parent 5c0519a226
commit 5b931111b3
9 files changed
+142 -19

No files matched your search

+5 -3
View File
@@ -327,9 +327,11 @@ func formatBytes(_ bytes: UInt64) -> String {
}
/// One line under a download that has not finished: "Running · 120.0 MiB of
/// 1.2 GiB · 2.4 MiB/s", or "Queued" while it waits for a slot.
func downloadStatusLine(_ download: RetainedDownload) -> String {
var parts = [download.status.prefix(1).uppercased() + download.status.dropFirst()]
/// 1.2 GiB · 2.4 MiB/s", or "Queued" while it waits for a slot — or, given a
/// `waitingFor`, what it is really waiting on.
func downloadStatusLine(_ download: RetainedDownload, waitingFor: String? = nil) -> String {
let status = download.status == RetainedDownload.statusQueued ? waitingFor : nil
var parts = [status ?? download.status.prefix(1).uppercased() + download.status.dropFirst()]
if download.total > 0, download.downloaded > 0 {
parts.append("\(formatBytes(UInt64(download.downloaded))) of \(formatBytes(UInt64(download.total)))")
}
+19 -2
View File
@@ -9,6 +9,8 @@ private struct OfflineGroup: Identifiable {
}
struct DownloadsView: View {
var onPlay: (TitleRecord, Int) -> Void = { _, _ in }
var onOpen: (TitleRecord) -> Void = { _ in }
@Environment(AppModel.self) private var model
@Environment(\.scheme) private var scheme
// Deleting a saved episode asks first: it is the viewer's own choice to
@@ -48,7 +50,7 @@ struct DownloadsView: View {
caption: "\(group.files.count) \(group.files.count == 1 ? "episode" : "episodes") · \(formatBytes(group.files.reduce(0) { $0 + $1.size }))"
)
ForEach(group.files, id: \.linkId) { file in
SavedRow(file: file, title: group.title) { deleting = file }
SavedRow(file: file, title: group.title, onPlay: onPlay, onOpen: onOpen) { deleting = file }
}
}
}
@@ -102,11 +104,16 @@ private struct SectionHeading: View {
private struct SavedRow: View {
let file: OfflineRecord
let title: TitleRecord?
let onPlay: (TitleRecord, Int) -> Void
let onOpen: (TitleRecord) -> Void
let onDelete: () -> Void
@Environment(\.scheme) private var scheme
var body: some View {
let episode = file.episode
// What a download is for: tapping it plays it, from where it was
// left, without going back through the library to find it.
let index = title?.playlist.firstIndex { $0.linkId == file.linkId }
HStack(spacing: 16) {
RemoteArtwork(episode?.stillUrl ?? title?.backdropUrl, title?.name ?? file.linkId, fallback: episode?.thumbnailSource, labelled: episode == nil)
.frame(width: 96)
@@ -118,11 +125,21 @@ private struct SavedRow: View {
.textStyle(.bodyMedium).foregroundStyle(scheme.onSurfaceVariant)
}
Spacer(minLength: 0)
if let title {
IconButton("info.circle", "Open \(title.displayName)") { onOpen(title) }
}
IconButton("trash", "Delete download", action: onDelete)
}
.padding(.leading, 16)
.padding(.trailing, 4)
.padding(.vertical, 8)
.contentShape(Rectangle())
.onTapGesture {
if let title, let index { onPlay(title, index) }
}
.accessibilityAction(named: "Play") {
if let title, let index { onPlay(title, index) }
}
}
}
@@ -138,7 +155,7 @@ private struct PartialRow: View {
HStack(spacing: 8) {
VStack(alignment: .leading, spacing: 2) {
Text(download.label).textStyle(.bodyLarge).foregroundStyle(scheme.onSurface).lineLimit(2)
Text(downloadStatusLine(download)).textStyle(.bodyMedium).foregroundStyle(scheme.onSurfaceVariant)
Text(downloadStatusLine(download, waitingFor: DownloadCoordinator.shared.queueReason)).textStyle(.bodyMedium).foregroundStyle(scheme.onSurfaceVariant)
if download.total > 0 {
AccentProgress(progress: Double(download.downloaded) / Double(download.total)).padding(.top, 8)
}
+1 -1
View File
@@ -462,7 +462,7 @@ private struct EpisodeRow: View {
if let active = download, active.total > 0, !episode.offline {
AccentProgress(progress: Double(active.downloaded) / Double(active.total), solid: scheme.tertiary)
.padding(.top, 8)
Text(downloadStatusLine(active)).textStyle(.bodySmall).foregroundStyle(scheme.onSurfaceVariant)
Text(downloadStatusLine(active, waitingFor: DownloadCoordinator.shared.queueReason)).textStyle(.bodySmall).foregroundStyle(scheme.onSurfaceVariant)
}
}
.padding(.horizontal, 16)