diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c1d7e2e..62c23d1 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -94,8 +94,9 @@ jobs: run: | sudo apt-get update sudo apt-get install --no-install-recommends \ - autoconf automake build-essential cmake curl git gperf libtool \ - meson nasm ninja-build pkg-config python3 unzip wget xz-utils yasm + autoconf automake build-essential cmake curl git gperf libdbus-1-dev \ + libtool meson nasm ninja-build pkg-config python3 unzip wget xz-utils \ + yasm - name: Install Android SDK and NDK run: >- @@ -109,7 +110,21 @@ jobs: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android + # One cache for both jobs: the signed build compiles the same crates for + # the same targets. Saved on failure too, so a run that breaks late does + # not throw away everything it compiled before the break. - uses: Swatinem/rust-cache@v2.9.1 + with: + shared-key: android + cache-on-failure: true + + # libmpv and its dependency tree are the slowest part of the build, and + # depend only on the pinned revision and the build script, which stamps + # its output with both and skips the build when the stamp matches. + - uses: actions/cache@v6 + with: + path: android/app/build/generated/mpv + key: libmpv-${{ hashFiles('scripts/build-libmpv-android.sh') }} - uses: gradle/actions/setup-gradle@v4 with: @@ -167,8 +182,9 @@ jobs: run: | sudo apt-get update sudo apt-get install --no-install-recommends \ - autoconf automake build-essential cmake curl git gperf libtool \ - meson nasm ninja-build pkg-config python3 unzip wget xz-utils yasm + autoconf automake build-essential cmake curl git gperf libdbus-1-dev \ + libtool meson nasm ninja-build pkg-config python3 unzip wget xz-utils \ + yasm - name: Install Android SDK and NDK run: >- @@ -182,7 +198,21 @@ jobs: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android + # One cache for both jobs: the signed build compiles the same crates for + # the same targets. Saved on failure too, so a run that breaks late does + # not throw away everything it compiled before the break. - uses: Swatinem/rust-cache@v2.9.1 + with: + shared-key: android + cache-on-failure: true + + # libmpv and its dependency tree are the slowest part of the build, and + # depend only on the pinned revision and the build script, which stamps + # its output with both and skips the build when the stamp matches. + - uses: actions/cache@v6 + with: + path: android/app/build/generated/mpv + key: libmpv-${{ hashFiles('scripts/build-libmpv-android.sh') }} - uses: gradle/actions/setup-gradle@v4 with: diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..964172e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,150 @@ +# Android plan + +Where the Android client stands against the desktop app, and what to change to +make it feel like a modern streaming app rather than a settings form with a +player attached. Written 2026-10-03 from a source review and four screenshots +(Library, Title, Shares, Settings) taken on a phone running v0.5.0. + +Android last caught up with the desktop on 2026-08-10 (`4ef6225`); the desktop +gained about twenty features on 2026-10-02 that Android does not have. + +## 0. CI (done, unverified) + +Every `Android` workflow run since August failed in `:app:buildRustHost`: +`pstr-core` pulls `keyring` → `dbus-secret-service` → `libdbus-sys`, which needs +`libdbus-1-dev` on the host. Because no run ever succeeded, no cache was ever +saved, so each run started from nothing. + +- `libdbus-1-dev` added to both jobs' apt install. +- `Swatinem/rust-cache` shares one key between the two jobs and saves on failure. +- `android/app/build/generated/mpv` is cached on the hash of + `scripts/build-libmpv-android.sh`; the script writes a `STAMP` (revision plus + its own hash) last and skips the whole libmpv build when the stamp matches. + +Verify with `gh workflow run android.yml -f signed_release=true`, then run it a +second time and check that the libmpv step finishes in seconds. + +## 1. Desktop parity + +Ordered by value on a phone. + +1. **Open share links from outside the app.** The manifest has only the + launcher intent. Add an `ACTION_VIEW` filter for `drive.proton.me/urls/*` and + an `ACTION_SEND` (`text/plain`) filter; both open the add-share dialog with + the link filled in. Desktop: `4a54af3`, `53a1d0a`. +2. **Streaming cache budget.** Android runs on the fixed 4 GiB default + (`DiskCacheConfig::DEFAULT_BUDGET_BYTES`), large for a phone. Expose + `DiskCache::set_budget` through `pstr-android`, add a 1–16 GiB choice with a + usage readout under Storage. Desktop: `a432a93`. +3. **Hardware-decoding switch.** `pstr_mpv.cpp` hard-codes + `hwdec=auto-copy-safe`; a device with green frames has no way out. A + Settings toggle, applied from the next file. +4. **Episode list in the player.** A bottom sheet (portrait) or side panel + (landscape) with the title's episodes, opened on the playing season and + scrolled to the playing episode. Desktop: `7e734db`. +5. **Long-press menu on tiles.** Play/resume, mark watched or unwatched, + download, change match, remove from Continue watching, in a + `ModalBottomSheet`. Desktop: `c5808a5`. +6. **Undo on snackbars** for marking a title and leaving Continue watching. + `AppUiState.message` is a bare string today; it needs an action. Desktop: + `79c3f68`. +7. **History tab.** Needs a bridge call over the `pstr-core::library` query + the desktop page uses. Desktop: `e7eeaa5`. +8. **Buffered range on the seek bar** from mpv's `demuxer-cache-time`. +9. **Up-next card** with the next episode's still and a countdown ring instead + of "Up next in 7s" text. +10. **Featured banner** at the top of the library. Desktop: `7d03736`. +11. **Bundled Inter** in `ui/theme/Type.kt`, so both clients read the same. + Desktop: `558446b`. + +## 2. UI recommendations + +### Everywhere + +- **Colours stay the theme's.** The red is the Persona theme's accent and the + card tint comes from the shared palette, so the redesign changes layout and + button hierarchy, not colours. +- **One button hierarchy.** At most one filled accent button per screen; + secondary actions tonal or outlined; destructive actions behind a menu and a + confirmation, never a filled red button in a list row. +- **Edge-to-edge, per-screen titles.** The "proton-stream" app bar on every + page wastes a band of height. Library uses a collapsing large title; Title + pages draw the backdrop under the status bar; other tabs use their name. +- **Skeletons instead of spinners** for library and title loading, and + generated placeholder art (gradient from the accent plus the title's + initials) instead of a black card with a play triangle (Akira). +- **Pull-to-refresh** (`PullToRefreshBox`) replaces the top-bar refresh icon, + with crawl progress shown in the indicator area. +- **Split `ui/ProtonStreamApp.kt`** (1665 lines) into one file per screen + before the work below; every item touches it. + +### Library + +- Poster grid (2:3, `posterUrl`) at three columns on a phone, title under the + poster with no card body. The current 210 dp cropped backdrops fit two titles + per screen. +- Continue watching: 16:9 still with the progress bar laid over the bottom + edge of the image and "S03E11 · 12 min left" under it. +- Search becomes an icon in the app bar that expands into an M3 `SearchBar`, + instead of a permanent field above everything. +- Fix "1 episodes" (line ~507; the season header already pluralises). + +### Title page + +- Full-bleed backdrop with a gradient scrim, poster, title and meta chips + (year, score, genres) over it, collapsing as the page scrolls. +- Synopsis clamped to three lines with "More". +- One full-width primary button, "Resume S03E11" or "Play S01E01", then a row + of icon actions: download, mark watched, and a ⋮ menu holding Start over, + Change match and More on AniList. Today five buttons of three styles wrap + over three lines. +- Drop the "Preferred tracks" form and its Save button from this page. The + player already remembers tracks by name per title (`d3c5f07`); expose any + override from the player's track chooser. +- Season picker as a dropdown chip or tabs, not stacked accordions; a 40 + episode title is one long scroll today. +- Episode rows: 16:9 still with progress overlay, "1 · Mother and Children" + (provider name, else "Episode 1") rather than the raw file name + `[Judas] Oshi no Ko - S01E01.mkv`, runtime or air date as supporting text, + watched and download state as small trailing icons. The red `S01E01` label + above each row goes. + +### Shares + +- `ListItem` rows: name, "12 titles · crawled 2 h ago" or the failure, and a ⋮ + menu with Refresh, Re-enter link and Remove (confirmed). The current row + wraps "Custom password stored securely" over three lines beside three + differently styled actions. +- "Add share" as an extended FAB, which is also where the share-link intent + from parity item 1 lands. + +### Settings + +- Grouped `ListItem` rows (headline, one line of supporting text, trailing + switch), with the long explanations moved into the supporting line or an + info dialog. Today each toggle carries a paragraph. +- Split into sub-pages once storage and decoding settings land: Playback, + Downloads & storage, Appearance, Metadata, About. +- Language fields become a picker dialog over a language list, not free text. +- Storage shows a usage bar (cache, offline episodes) above the budget choice. + +### Robustness + +- Screenshot tests (Roborazzi, JVM, no device) for each screen in light and + dark, phone and tablet, at font scale 1.0 and 1.5 — the redesign changes + every screen, and `docs/TESTING.md` has no visual layer today. +- 48 dp minimum touch targets and content descriptions on the new icon-only + actions. +- Tablet: `ListDetailPaneScaffold` for library + title, since + `NavigationSuiteScaffold` already switches to a rail there. +- Shared-element transition from a poster to its title page, and predictive + back. + +## Suggested order + +1. CI green (section 0). +2. Split `ProtonStreamApp.kt`; add screenshot tests for the current screens. +3. Parity 1–3 (small, phone-specific fixes). +4. Button hierarchy and Inter — every later screen inherits them. +5. Title page, then Library, then Shares and Settings. +6. Parity 4–10. diff --git a/scripts/build-libmpv-android.sh b/scripts/build-libmpv-android.sh index 5761d45..f864eae 100644 --- a/scripts/build-libmpv-android.sh +++ b/scripts/build-libmpv-android.sh @@ -7,6 +7,20 @@ source_dir="${PSTR_MPV_SOURCE_DIR:-$repo_root/.build/mpv-android}" revision=20a3fa526fac6d3fe267aee0d4c349893fee65a3 destination="$repo_root/android/app/build/generated/mpv" +# The staged tree depends only on the pinned revision and on this script, which +# also pins the NDK. A tree stamped with both is reused as is: that is what lets +# CI restore it from a cache instead of rebuilding FFmpeg and every other +# dependency from source on each run. +stamp="$revision $(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)" +if [[ "$(cat "$destination/STAMP" 2>/dev/null)" == "$stamp" && + -f "$destination/jniLibs/arm64-v8a/libmpv.so" && + -f "$destination/jniLibs/x86_64/libmpv.so" && + -f "$destination/mpv-android-source-$revision.tar.gz" ]]; then + echo "libmpv $revision is already staged in $destination" + exit 0 +fi +rm -f -- "$destination/STAMP" + if [[ ! -d "$source_dir/.git" ]]; then git clone https://github.com/mpv-android/mpv-android.git "$source_dir" fi @@ -191,3 +205,5 @@ tar \ -czf "$destination/mpv-android-source-$revision.tar.gz" \ . printf '%s\n' "$revision" > "$destination/REVISION" +# Last, so an interrupted run never leaves a stamp over a partial tree. +printf '%s\n' "$stamp" > "$destination/STAMP"