Skip to content

feat(app): show rewatch count on the watched badge - #362

Open
matheus-souza wants to merge 1 commit into
trakt:mainfrom
matheus-souza:feat/rewatch-count-badge
Open

matheus-souza wants to merge 1 commit into
trakt:mainfrom
matheus-souza:feat/rewatch-count-badge

Conversation

@matheus-souza

@matheus-souza matheus-souza commented Aug 28, 2026 •

Copy link
Copy Markdown

Closes #361

Mirrored on the web in trakt/trakt-web#3215 (issue) and trakt/trakt-web#3216 (implementation). Worth noting from that trip: getShowWatchState.minPlays and useWatchCount already implement the same "lowest play count across regular episodes, only once fully watched" rule argued for here, and the web already renders WATCHED · N on summary posters - so the two platforms agree on the figure, and only the list cards were a real gap there. Season posters and the episode rows inside a season stay out of the web PR, since useWatchCount has no season case.

The watched check is binary, so a title watched three times looks exactly like one watched once in every listing. The count existed only in movie and episode details, as WATCHED • N; shows and seasons had none at all. This puts the count on the chip itself, so a list answers "how many times did I watch this" without a tap.

03-search-depois-2x

What counts as "watched N times"

For a movie, the play count. For a show, the lowest play count across its non-special episodes; for a season, the same within that season. The figure is only surfaced once the title already reads as fully watched, so a partial rewatch shows nothing rather than claiming a complete one.

floor(total plays / aired episodes) was the obvious alternative and it lies: 61 episodes at three plays plus one at a single play rounds to "2x". The minimum is the only number true for every episode.

The count is rendered only above one play, so a title watched once keeps the chip it has today, byte for byte.

Changes

:common — the count — ProgressItem.ShowItem gains completedPlays (and ShowItem.Season.completedPlays), computed the same way as the existing plays / playsWithoutSpecials getters. CollectionStateProvider populates a completedShowsPlays map alongside the existing watchedShowsPlays, and watchedMovies goes from a Set<TraktId> to a Map<TraktId, Int> of play counts — the field is private, so isWatched just moves from contains to containsKey. UserCollectionState gains plays(traktId, type, airedEpisodes), a sibling of isWatched / isWatching that returns 0 unless the title is fully watched.

No networking change. /users/me/watched/shows and /users/me/watched/movies already return every play date per episode and per movie; only the sum was being used.

:app — one chip instead of four — the same pill was implemented four times: privately in VerticalMediaCard and HorizontalMediaCard, inline in PanelMediaCard and PanelHorizontalMediaCard. They collapse into ui/components/mediacards/CollectionChip.kt, which absorbs the variations that existed between them (24x16 vs 22x16, 1.5dp vs 1dp elevation, the halved "watching" state) as parameters. With no count it is the same fixed-size box as before; with a count it becomes an auto-width row of icon plus Nx, and its height becomes a minimum so the counter can grow with the system font scale.

:app — the count reaches the cards — the four cards gain plays: Int = 0. The default means every call site not touched here compiles and renders exactly as before. The ~27 listings that already read collection.isWatched(...) now also pass collection.plays(...), threaded through each file's local item composable.

:app — shows, seasons, episodes — ShowDetailsState.ProgressState gains completedPlays, so ShowDetailsHeader can stop passing playsCount = null and the poster chip renders WATCHED • N like movies and episodes already do. ShowSeasons.markWatchedSeasons and markWatchedEpisodes carry the count into SeasonItem.plays and EpisodeItem.plays — markWatchedEpisodes swaps its set of watched ids for a map of id to play count, which is where isWatched now comes from too. Both the season rail and the episode rows read it, on the show details section and on the full-screen seasons view.

No new strings. The show header reuses tag_text_watched, already translated in every locale, in the same inline template movie and episode details use; the chip renders a digit and a multiplication sign, which is not translatable text.

Testing

./gradlew :app:assembleInternalDebug and ktlint 1.7.1 both pass.

Verified on an emulator (API 31) against a real account, using a 9-episode show marked watched a second time and a movie marked watched a second time:

Surface Result
Search grid (VerticalMediaCard) Show and movie chips read 2x
Discover list (PanelMediaCard) Movie chip reads 2x
Show details header WATCHED • 2
Season rail poster 2x
Episode cards in show details (HorizontalMediaCard) 2x on each
Episode rows on the seasons screen (PanelHorizontalMediaCard) 2x on each
Movie details header WATCHED • 2, unchanged behaviour
Titles with a single play Chip identical to before, no number
Partially watched show "Started" chip unchanged, no number
04-serie-header-watched-2 06-temporada-e-episodios-2x 07-tela-temporadas-2x 09-filme-2x-e-watching-intacto 10-lista-discover-2x

Two caveats worth stating:

  • RTL was not exercised at runtime. The counted chip is a plain Row, so it mirrors the same way the existing watched/watchlist chip pair already does, but I could not force an RTL locale on this emulator to confirm it visually.
  • The :tv module keeps its own copies of the media cards and is untouched. It can follow separately.

The watched check on a card is binary, so a show, season, episode or movie
watched three times looks exactly like one watched once. The count existed
only inside movie and episode details, as "WATCHED - N", and shows and
seasons had no count anywhere.

The data is already local and costs no request: the watched progress cache
keeps every play date per episode and per movie. A title's rewatch count is
now the lowest play count across a show's non-special episodes (or the movie's
play count), and it is only surfaced once the title reads as fully watched, so
a partial rewatch never claims a complete one.

The four near-identical private chip implementations in the media cards
collapse into one CollectionChip that renders "icon + Nx" above one play and
keeps its exact previous size at one.
@michaldrabik

Copy link
Copy Markdown
Collaborator

Hi and thanks for the PR,
as the general rule we try to keep our apps in parity with what's happening on the website

If you wish to introduce something new into the app that's NOT already available in the website please open PR for the web first - or even better start with an Github issue or Featurebase request. This way tokens and time will not be wasted ;)

If you spot something thats already on web but not yet in the app then PR is welcome surely. If this one contains something like this partially please extract this in a new separate smaller one.

Thanks! This info should be available in the readme actually. I will be adding this now and also copy this message to other PRs.

@matheus-souza

Copy link
Copy Markdown
Author

The portable part of this is now up for the web:

Worth knowing for this PR: the web already had most of the thinking done. getShowWatchState returns minPlays, and useWatchCount already computed isWatched ? minPlays : 0 for shows - the same "lowest play count across regular episodes, only once fully watched" rule argued for in #361. It also already renders WATCHED · N on summary posters, for shows included, so the details half of #361 is not a gap there. The gap was only the list cards, which fall back to a plain indicator.

Scope note: season posters and the episode rows inside a season stay out of the web PR. useWatchCount has no season case, so the per-season minimum would be new logic rather than reuse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: show how many times a title was watched on the watched badge

2 participants