On a phone the backdrop at the top of a title page now moves at half
the speed of the page scrolling over it, and the back button sits in a
bar that fills in with the title's name once the backdrop has gone, so
a long episode list still says whose it is. The fade into the page is
tinted with the backdrop's colour, as on the library's banner.
iOS keeps its slide in from the edge: the zoom transition needs a
NavigationStack, and the title page is shown over the library by hand.
A BGProcessingTask runs the same crawl_for_news no sooner than every six
hours, when iOS chooses. A processing task rather than an app refresh,
since a crawl of a large share does not fit in thirty seconds.
Metadata was only fetched after adding a share (mobile) or from the
Settings buttons (desktop), so a show or episode that turned up in an
existing share stayed bare. Every crawl is now followed by a match run
when enrichment is on; `plan` makes no requests for a library that is
already matched. A stored episode guide is fetched again when a file
newer than it has no named entry, and daily for two weeks while an
aired episode is still unnamed.
Removing a share now carries its watch state to any other share holding
the same file, `pstr remove` drops the share's catalog rows, and the app
forgets catalog rows of shares no longer configured — an older `pstr
remove` left them, and every episode showed twice.
iOS, for a slow iPad:
- swipe back on a settings page uses its own edge strip; on the whole
page it also pressed the row under the finger and opened the metadata
sheet
- "Change match" from the tile menu opens once the menu has dismissed,
not after a guessed 350 ms that a slow device missed
- the activity banner tracks each running line by itself (also Android),
so overlapping work no longer leaves it spinning forever
- buttons, tiles and rows draw their press; marking watched shows at
once; remove/repair share, sign out and deletions show the banner;
the player says "Opening…" while the stream opens
- posters are read into a reserved buffer instead of a byte at a time
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.
The release calls ios.yml after the version check, as it does android.yml,
and attaches proton-stream-<version>-ios.ipa next to the other artifacts and
in SHA256SUMS.
The iOS client is now a screen-for-screen port of the Android app over the
same pstr-android UniFFI surface, with no Rust changes: Library, title page,
History, Shares (public links and Drive folders), Downloads and Settings,
with the same strings, undo snackbars and two-pane layout on wide screens.
- Theme from the engine's palette (Material role names, Inter, the same
type ramp and corners), repainted live from the appearance settings.
- Account sign-in with two-factor code, mailbox password and the Proton
CAPTCHA in a non-persistent WKWebView, the Drive folder browser, and
watch-history sync every five minutes, after sign-in, on player close and
when the app leaves the screen.
- Player over libmpv: resume, progress saved every 5 s, autoplay with the
up-next card, skip and auto-skip from the chapter plan, track, speed,
volume and episode pickers, landscape lock, mini transport, and
lock-screen controls with interruption and route-change handling.
- Offline downloads with the Android record format, free-space floor,
retries, transfer rate and the Wi-Fi-only policy; interrupted transfers
resume at next launch.
- Config, data and download state are excluded from backup, as Android's
allowBackup=false: a restore would bring shares back without their
Keychain secrets and give two devices one sync identity.
Picture-in-picture, the share sheet and background downloads are not
possible the same way on iOS; docs/IOS.md lists why and what replaces them.
Adds XCTest ports of the Android unit tests, a `test` command to
scripts/build-ios.sh, the app icon and its render script, and an iOS
workflow on macos-15 that runs the tests and builds the unsigned .ipa.