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:
nvrl committed 2026-10-12 22:51:10 +02:00
1 parent 6d1943f2bd
commit 01c252b794
8 files changed
+44 -8

No files matched your search

+9 -2
View File
@@ -484,8 +484,15 @@ final class AppModel {
/// title's positions are what this throws away, and a mis-tap on a tile's
/// menu should not cost them.
func setTitleWatched(_ title: TitleRecord, _ watched: Bool) {
showWatched(title.playlist, watched)
changeWatch(title.playlist, "\(title.displayName) marked \(watched ? "watched" : "unwatched")") { engine, episode, before in
markEpisodes(title.playlist, watched, title.displayName)
}
/// 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
try engine.saveWatchState(
shareId: episode.shareId,
+8 -1
View File
@@ -246,12 +246,19 @@ struct TitleView: View {
HStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
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 }
}
}
}
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)") {
DownloadCoordinator.shared.enqueue(season.episodes)
}