mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-04 00:47:00 +01:00
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:
1 parent
5c0519a226
commit
5b931111b3
9 files changed
+142
-19
No files matched your search
@@ -185,7 +185,10 @@ struct RootView: View {
|
||||
// on a form that has done its job.
|
||||
SharesView(incomingLink: $incomingLink, onFirstShareAdded: { select(.library) })
|
||||
case .downloads:
|
||||
DownloadsView()
|
||||
DownloadsView(onPlay: play, onOpen: { title in
|
||||
destination = .library
|
||||
selectedTitleKey = title.key
|
||||
})
|
||||
case .settings:
|
||||
SettingsView()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ final class DownloadCoordinator {
|
||||
/// Every retained download, sorted by label. Re-read after each write.
|
||||
private(set) var records: [RetainedDownload] = []
|
||||
|
||||
/// Why a queued download is not running, when it is not merely a free
|
||||
/// slot: a Wi-Fi-only download on mobile data waits without a word, and
|
||||
/// "Queued" for an hour on the bus reads as stuck.
|
||||
private(set) var queueReason: String?
|
||||
|
||||
/// A download finished or was removed: the catalog has changed.
|
||||
@ObservationIgnored var onFinished: (() -> Void)?
|
||||
|
||||
@@ -125,6 +130,10 @@ final class DownloadCoordinator {
|
||||
/// Re-apply "Wi-Fi only" to what is queued and running: a season queued
|
||||
/// before the setting was turned on must not then download over cellular.
|
||||
func applyNetworkPolicy() {
|
||||
queueReason = switch path?.status {
|
||||
case .satisfied?: allowed ? nil : "Waiting for Wi-Fi"
|
||||
default: "Waiting for a connection"
|
||||
}
|
||||
if allowed {
|
||||
resumeQueued()
|
||||
} else {
|
||||
|
||||
@@ -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)))")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user