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.
The empty library said to add a link "under Shares, then refresh" with
nothing to press on the phones, and on the desktop only switched page.
All three now open the add form from there, the desktop with the caret
in the link field; and adding already crawls, so there is nothing to
refresh.
A share's name was required before the viewer had seen anything to name.
It is optional now: without one, ShareStore::add_verified takes the
shared folder's own name from the link it has just opened. The link
comes first in each form. The desktop form submits on Enter, and the
Android link field has an eye to check what was pasted.
No client crawled a share unless someone pressed Refresh, so a new
episode stayed out of the library until then, and nothing said how stale
the library was.
Catalog schema v11 adds share_crawls: when each share's last crawl
finished and was attempted, how many files it stored and how many were
new, and why the last attempt failed. A failure keeps the last good
crawl's numbers.
Every client now crawls, quietly, any share not looked at for six hours:
the desktop at launch and every half hour while open, the phones at
launch and on their existing catch-up timer. A quiet crawl says only
news ("anime: 3 new files") and leaves failures on the share's row
rather than in a toast.
Share rows on all three show "120 files - crawled 2 h ago", or the last
failure until a crawl succeeds.
Continue watching listed only part-watched episodes, so finishing one -
the most ordinary moment to want the next - dropped the show from the
shelf. Title::continue_with now reads the episode touched last: a
part-watched one is resumed, a finished one offers the episode after it
("Next: S01E04"), and one reset to the start (what leaving the shelf
does) keeps the title off. A few seconds of an episode are passed over.
The phones computed their own shelf from episode records. The bridge now
exports the engine's choice as TitleRecord.continue_index, and Play on a
title lands there on every client.
Undo on the phones re-saved a watch state with a fresh time, which both
reordered the shelf and could not express "never played". A new
restore_watch_state puts the state back as it was read.
Sync carried only watch positions, so a second device started empty. The
per-installation history file now also carries per-key registers
(pstr-core::profile): every share with its link and custom password, the
playback, appearance and metadata settings, the TMDB key, hand-picked
matches and per-title track choices. Newer updated_at wins per key, a
removal travels as an empty register, and a first sync stamps at 0 so a
fresh install takes the library the other devices have instead of
overwriting it with defaults. Volume, mute and the device-only settings
(cache budget, Wi-Fi only, background audio, hardware decoding) stay out.
The secrets live only in the end-to-end encrypted Drive file, never in
shares.json. FORMAT_VERSION stays 1; an older build ignores the registers.
Adding a share stored it before opening the link, so a first failure left a
broken entry and every retry said "already configured". add_verified opens
the link first and stores nothing on failure, and only the new share is
crawled afterwards, so another broken share cannot fail the add.
Android drops picture-in-picture: the window sized the video wrongly and
background audio already covers leaving the app.
iOS sizes the Metal drawable on every layout, so the picture no longer
stretches after a rotation or a Split View resize. Settings show a save at
once rather than after the match run, and a failed save reverts and says
why. Both phone clients gain an activity banner naming what is running, and
the add-share, metadata and change-match forms show progress and errors in
place.
A Proton account is now optional alongside public links. Signing in (password,
then a second-factor code and a mailbox password where the account has them;
a CAPTCHA gate is solved in the Android app) stores one session blob in the OS
credential store under `proton-account`, writing back every refreshed token
since refresh tokens are single-use. With an account, folders of the viewer's
own Drive — My Files, other devices, shared with me — can be added as shares
through the same `ShareClient` surface as a public link.
Watch history syncs through a `proton-stream` device in that Drive: one
`watch-history/<installation>.json` per installation, so no two devices ever
write the same file. Each sync reads the others and merges per episode, newer
`updated_at` wins, with a deterministic tie-break. It runs at launch, after
sign-in, whenever the player closes and every five minutes. A session that can
no longer refresh signs the app out instead of failing on every launch.
The CLI gains `login`, `logout`, `account`, `browse` and `sync`.
Also in this change:
- Android offline downloads no longer look stuck (B64): two transfer at once,
through a copy stream with no read-ahead, with a smoothed transfer rate.
- Choosing a metadata match by hand now enriches the title too, instead of
keeping the old match's episodes and artwork.
The catalog schema is unchanged.
Moving the gradient onto a row inside the button lost both: the button's
own row hands its children no minimum size, so a fillMaxWidth button
painted the ramp only behind its label, and with square corners.
The gradient goes back on the button, and the 48dp touch-target padding
is taken by a box around it instead, with the button inside told not to
add its own. The primary button now matches the other buttons' 40dp
container at every width. The screenshots are re-recorded for it.
Bring the phase 2 library to Android. Search, sorting and franchise folding
move from the desktop app into pstr_core::browse, so both clients find,
fold and order the same titles the same way.
The bridge gains AndroidEngine::arrangement, which returns the grid's tiles
and up to three shelves as keys into the library's titles. Each TitleRecord
now carries its display name, wide art, format, season, studios, directors,
tags, airing state and franchise. The library bar has a sort menu with the
franchise toggle, folded tiles show a +N badge, and shelves sit above the
grid. The title page shows the same facts, tags and franchise row as the
desktop. Settings -> Appearance gains Title names.
The default title name is now English, which is what Android always showed.
A folded franchise now sorts by its lead's displayed name rather than by its
alphabetically first member.
The Settings index shows the installed version, read from the package,
so the committed image held 0.5.1 and the 1.0.0 bump failed the
screenshot check. SettingsScreen takes the version as a parameter that
defaults to the package's, and the test passes a fixed one.
The screenshot tests now also capture the library and a title in
Catppuccin Latte, and a title and the history page on a tablet. The
tablet capture showed the title page opening on nothing but its
backdrop: a 16:9 picture the width of a landscape tablet is taller than
the window. The backdrop is now no taller than about half the window,
and the rest of the page keeps a phone's reading width, centred.
Saved episodes are list rows with their still, the episode's name rather
than its file, and its size, under a heading per show; the page opens
with how much is on the device. Downloads still arriving come first, with
pause or resume and cancel as icons beside the progress. Deleting a saved
episode now asks first, since getting it back is a download.
The first Settings page lists Playback, Downloads and storage,
Appearance, Metadata and About, each with a line saying where it stands,
and each opens its own page with a way back. On one long page, every
setting added pushed the rest further from reach.
Audio and subtitle language are chosen from a list of languages by name
instead of typed as "jpn or eng". The list stores the three-letter tag a
Matroska file carries, reads a stored tag in either ISO form, and still
takes any other tag typed in.
The single "proton-stream" bar over every page is gone. The library has a
large title that folds away as the grid scrolls, with search and refresh as
icons; search opens into a field in the bar that takes the keyboard at once,
and back closes and clears it. History, Shares, Downloads and Settings each
name themselves in a bar of their own, drawn in the page's background until
content scrolls under it. The title page now draws its backdrop under the
status bar, with the back button below it.
A search with no results says so instead of claiming the library is empty.
A title without artwork shows the accent as a gradient with its initials
instead of a black card with a play triangle, so a library of unmatched
titles is no longer a grid of identical cards. Art with the name already
over it, and episode stills, get the gradient without initials. While
artwork loads, and while the catalog first loads, the page shows pulsing
shapes where the posters will be rather than a spinner.
The screenshot tests turn artwork loading off, so whether a failed fetch
lands before the capture no longer decides what the image shows.
The desktop's rule: the title watched last, or with nothing part watched,
one of the titles the provider has a backdrop for, turning over once a
day. Large art fading to black behind the name, rating, genres and three
lines of synopsis, with Resume or Play and More info.
History sits second in the navigation, as on the desktop: every played
episode, newest first, under Today, Yesterday, a weekday or a date. The
still plays the episode, the text opens its title, and the cross takes
it off the page — forgetting its position and watched mark — with Undo.
The list is the desktop's Library::history rule over the records the
library already holds, so no bridge call is added; HistoryTest pins the
rule and the day headings.
- a long press on a title opens a sheet: Play or Resume, Open, Download
all, and Mark watched or unwatched for the whole title
- a long press on a Continue watching card offers Resume, Open, and
Remove from Continue watching, which forgets the position as the
desktop does
- marking a title and leaving Continue watching both say what they did
in a snackbar with Undo, which puts every changed watch state back
- the "next up" episode is one shared function, used by the title page
and the menu alike
Playback, Downloads and storage, Appearance, Metadata and About, each a
group of rows with one line of explanation, and the whole row as the
target. The paragraphs under the switches are gone; what they said is
either the row's supporting line or a code comment.
Storage shows the cache's use against its budget as a bar over the
budget chips. Clearing the cache and deleting offline episodes are rows
that say how much they free, and the delete still asks first. Licence
and notices are rows under About.
- each share is a row with its name and how it is unlocked; Refresh,
Re-enter link and Remove sit behind a menu instead of three differently
styled controls that squeezed the name into a column a word wide
- Remove asks first, saying that offline episodes and watch progress go
with it
- Add share is an extended floating button, where the share sheet and a
tapped link land too
- an empty page says what to do instead of showing a bare heading
- titles are 2:3 posters three across on a phone, named under the poster
instead of in a card body; cropped backdrops at full width fitted two
titles to a screen
- a title's caption says "Film" or "1 episode" rather than "1 episodes",
and wraps at large font scales instead of being cut to "2023 · 7"
- Continue watching cards carry the resume bar on the still and say how
much is left: "S03E11 · 12 min left"
- search is a filled pill with a clear button
- pull to refresh crawls the shares, as the bar's refresh icon does
- the backdrop runs edge to edge and fades into the page, with the back
button over it
- one full-width Play that names the episode it plays ("Resume S02E01")
- Start over, Download, Match and the provider link as one row of
labelled icons instead of four buttons in three styles over three lines
- the synopsis is clamped to three lines and opens on a tap; the watched
count joins the year, rating and genres
- seasons are chips showing one season at a time, opened on the season
Play lands in, instead of stacked accordions
- episode rows head with "1. Mother and Children", or "Episode 1", rather
than the filename; numbering, size and air date sit under it, and the
resume bar lies on the still's lower edge
- the per-title track form is gone: the player's track chooser already
stores each title's choice, and Settings holds the global default
The type ramp now draws in Inter Regular and SemiBold, the same two files
the desktop bundles, so the clients read as one app. Inter is declared
above the ramp: top-level properties initialise in file order, and the
ramp built first would have captured it as null and silently used the
system face. Its OFL text ships in the licences beside the notices.
Screenshots re-recorded.
Roborazzi draws Library (phone, tablet, font scale 1.5, empty), a title,
Shares, Settings and Downloads from fixed records under Robolectric, and
build-android.sh check compares them with the committed images in
app/src/test/screenshots. The redesign changes every screen; a layout
regression now fails the gate and shows up in review as a changed image.
A drive.proton.me/urls/ link opened from a browser, or text containing
one shared to the app, switches to Shares and opens the Add form with
the link filled in, fragment included. A playing episode moves to the
mini transport instead of covering the form. A recreated activity does
not offer the same link again.
The view filter is not autoVerify, since the domain is Proton's, so
Android only routes browser links here once "Open supported links" is
allowed in the app's settings; sharing works without that.
storedPalette() reads and resolves the theme file without building the
engine, so onCreate can set the window background and system-bar icon
polarity before anything draws. Previously the background was a hex literal
in themes.xml and the bar icons were pinned dark, which meant every cold
start flashed one flavour's colour and Latte drew white icons on a
near-white page.
Adds the Material scheme mapping, type ramp and shape tokens the palette
feeds, plus unit tests for both.