mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(android,ios): mark a whole season watched from the title page
Marking was one episode at a time on the title page, or the whole title from the library's long-press menu, so joining a show at season three meant ticking every earlier episode. The season row gains a mark button beside its download one, with an Undo, through a new markEpisodes that setTitleWatched now uses too. Episodes already in the asked-for state are left alone, so Undo restores only what changed. Season chips were keyed by label, and two seasons with one label is a crash in a lazy list; they are keyed by position now.
This commit is contained in:
1 parent
6d1943f2bd
commit
01c252b794
8 files changed
+44
-8
No files matched your search
@@ -551,11 +551,17 @@ class AppViewModel(context: Context, private val workManager: WorkManager) : Vie
|
|||||||
* title's positions are what this throws away, and a mis-tap on a tile's
|
* title's positions are what this throws away, and a mis-tap on a tile's
|
||||||
* menu should not cost them.
|
* menu should not cost them.
|
||||||
*/
|
*/
|
||||||
fun setTitleWatched(title: TitleRecord, watched: Boolean) {
|
fun setTitleWatched(title: TitleRecord, watched: Boolean) =
|
||||||
val episodes = title.seasons.flatMap { it.episodes }
|
markEpisodes(title.seasons.flatMap { it.episodes }, watched, title.displayName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some episodes seen or unseen — a season, or a whole title — with an
|
||||||
|
* Undo; [what] names them in the message.
|
||||||
|
*/
|
||||||
|
fun markEpisodes(episodes: List<EpisodeRecord>, watched: Boolean, what: String) {
|
||||||
changeWatch(
|
changeWatch(
|
||||||
episodes,
|
episodes.filter { it.watched != watched },
|
||||||
"${title.displayName} marked ${if (watched) "watched" else "unwatched"}",
|
"$what marked ${if (watched) "watched" else "unwatched"}",
|
||||||
) { engine, episode, before ->
|
) { engine, episode, before ->
|
||||||
val duration = before?.durationSecs
|
val duration = before?.durationSecs
|
||||||
engine.saveWatchState(
|
engine.saveWatchState(
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ fun ProtonStreamApp(
|
|||||||
model::reloadAfterMetadataChange,
|
model::reloadAfterMetadataChange,
|
||||||
model::setWatched,
|
model::setWatched,
|
||||||
body,
|
body,
|
||||||
|
onMarkEpisodes = model::markEpisodes,
|
||||||
franchise = selected.franchise.mapNotNull { key ->
|
franchise = selected.franchise.mapNotNull { key ->
|
||||||
state.titles.firstOrNull { it.key == key }
|
state.titles.firstOrNull { it.key == key }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.CheckCircle
|
import androidx.compose.material.icons.outlined.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.DoneAll
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.RemoveDone
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
@@ -97,6 +99,8 @@ internal fun TitleScreen(
|
|||||||
onMetadataChanged: () -> Unit,
|
onMetadataChanged: () -> Unit,
|
||||||
onSetWatched: (EpisodeRecord, Boolean) -> Unit,
|
onSetWatched: (EpisodeRecord, Boolean) -> Unit,
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
/** Several episodes seen or unseen at once, named for the message. */
|
||||||
|
onMarkEpisodes: (List<EpisodeRecord>, Boolean, String) -> Unit = { _, _, _ -> },
|
||||||
/** Every title of its franchise, in release order, this one included. */
|
/** Every title of its franchise, in release order, this one included. */
|
||||||
franchise: List<TitleRecord> = emptyList(),
|
franchise: List<TitleRecord> = emptyList(),
|
||||||
onOpenTitle: (TitleRecord) -> Unit = {},
|
onOpenTitle: (TitleRecord) -> Unit = {},
|
||||||
@@ -343,7 +347,9 @@ internal fun TitleScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
LazyRow(Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
LazyRow(Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
itemsIndexed(title.seasons, key = { _, it -> it.label }) { index, entry ->
|
// By position: two seasons can share a label, and a
|
||||||
|
// repeated key is a crash in a lazy list.
|
||||||
|
itemsIndexed(title.seasons, key = { index, _ -> index }) { index, entry ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = index == seasonIndex,
|
selected = index == seasonIndex,
|
||||||
onClick = { seasonIndex = index },
|
onClick = { seasonIndex = index },
|
||||||
@@ -352,6 +358,15 @@ internal fun TitleScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
season?.let {
|
season?.let {
|
||||||
|
// For someone joining the show partway, who would
|
||||||
|
// otherwise tick every earlier episode one by one.
|
||||||
|
val seen = it.episodes.all(EpisodeRecord::watched)
|
||||||
|
IconButton(onClick = { onMarkEpisodes(it.episodes, !seen, "${title.displayName} ${it.label}") }) {
|
||||||
|
Icon(
|
||||||
|
if (seen) Icons.Default.RemoveDone else Icons.Default.DoneAll,
|
||||||
|
contentDescription = "Mark ${it.label} ${if (seen) "unwatched" else "watched"}",
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(onClick = { DownloadCoordinator.enqueue(context, it.episodes) }) {
|
IconButton(onClick = { DownloadCoordinator.enqueue(context, it.episodes) }) {
|
||||||
Icon(Icons.Default.Download, contentDescription = "Download ${it.label}")
|
Icon(Icons.Default.Download, contentDescription = "Download ${it.label}")
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 631 KiB After Width: | Height: | Size: 632 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 669 KiB After Width: | Height: | Size: 670 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -484,8 +484,15 @@ final class AppModel {
|
|||||||
/// title's positions are what this throws away, and a mis-tap on a tile's
|
/// title's positions are what this throws away, and a mis-tap on a tile's
|
||||||
/// menu should not cost them.
|
/// menu should not cost them.
|
||||||
func setTitleWatched(_ title: TitleRecord, _ watched: Bool) {
|
func setTitleWatched(_ title: TitleRecord, _ watched: Bool) {
|
||||||
showWatched(title.playlist, watched)
|
markEpisodes(title.playlist, watched, title.displayName)
|
||||||
changeWatch(title.playlist, "\(title.displayName) marked \(watched ? "watched" : "unwatched")") { engine, episode, before in
|
}
|
||||||
|
|
||||||
|
/// Some episodes seen or unseen — a season, or a whole title — with an
|
||||||
|
/// Undo; `what` names them in the message.
|
||||||
|
func markEpisodes(_ episodes: [EpisodeRecord], _ watched: Bool, _ what: String) {
|
||||||
|
let episodes = episodes.filter { $0.watched != watched }
|
||||||
|
showWatched(episodes, watched)
|
||||||
|
changeWatch(episodes, "\(what) marked \(watched ? "watched" : "unwatched")") { engine, episode, before in
|
||||||
let duration = before?.durationSecs
|
let duration = before?.durationSecs
|
||||||
try engine.saveWatchState(
|
try engine.saveWatchState(
|
||||||
shareId: episode.shareId,
|
shareId: episode.shareId,
|
||||||
|
|||||||
@@ -246,12 +246,19 @@ struct TitleView: View {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(Array(title.seasons.enumerated()), id: \.element.label) { index, entry in
|
// By position: two seasons can share a label.
|
||||||
|
ForEach(Array(title.seasons.enumerated()), id: \.offset) { index, entry in
|
||||||
FilterChip(label: entry.label, selected: index == seasonIndex) { seasonIndex = index }
|
FilterChip(label: entry.label, selected: index == seasonIndex) { seasonIndex = index }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let season {
|
if let season {
|
||||||
|
// For someone joining the show partway, who would otherwise
|
||||||
|
// tick every earlier episode one by one.
|
||||||
|
let seen = season.episodes.allSatisfy(\.watched)
|
||||||
|
IconButton(seen ? "checkmark.circle.badge.xmark" : "checkmark.circle", "Mark \(season.label) \(seen ? "unwatched" : "watched")") {
|
||||||
|
model.markEpisodes(season.episodes, !seen, "\(title.displayName) \(season.label)")
|
||||||
|
}
|
||||||
IconButton("arrow.down.circle", "Download \(season.label)") {
|
IconButton("arrow.down.circle", "Download \(season.label)") {
|
||||||
DownloadCoordinator.shared.enqueue(season.episodes)
|
DownloadCoordinator.shared.enqueue(season.episodes)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user