feat(core,app,android,ios): My List, synced, and New badges with new-episode rows

My List is a list of titles to watch later, kept in my-list.json by
title key (pstr-core::mylist) and synced as one list/<key> register per
title, so an add or a removal on one device reaches every other. A list
file that will not parse is never written over: changing the list is
refused instead. It is a filter, a row on the library, a button on the
banner and the title page, and an entry in each tile's menu.

pstr-core::news reads node_first_seen against each share's first crawl,
so a newly added share does not badge its whole library. A title that
arrived in the last two weeks and was never played is New; a series
being watched counts the unwatched episodes that arrived after it was
last played ("3 new"). They fill the tile badge and the "New episodes"
and "Recently added" rows. shelves::home puts every row in one order
for all three apps, and a shelf tile of an airing show shows a
countdown ("Ep 8 · in 2 d").

The bridge gains TitleRecord.on_list and .badge, set_on_list, the
MyList filter, airing_countdown and SyncRecord.list_changed.
This commit is contained in:
nvrl committed 2026-10-30 22:15:51 +01:00
1 parent cd9e8eea81
commit f66faf6b75
31 files changed
+1142 -70

No files matched your search

+4
View File
@@ -251,6 +251,10 @@ struct TitleView: View {
.disabled(playlist.isEmpty)
.padding(.top, 16)
HStack(spacing: 0) {
TitleAction(icon: title.onList ? "checkmark" : "plus", label: "My List") {
model.setOnList(title, !title.onList)
}
.accessibilityValue(title.onList ? "On My List" : "Not on My List")
// Only where it would do something different: an unstarted show
// already starts at the beginning.
if upNext?.resumeAt != nil || title.watchedCount > 0 {