feat(ios): port the Android client to SwiftUI

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.
This commit is contained in:
nvrl committed 2026-10-07 00:32:03 +02:00
1 parent da16ab0575
commit ed05a1e723
38 files changed
+7546 -208

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
name: iOS
on:
push:
branches: [main]
workflow_call:
pull_request:
paths:
- 'ios/**'
- 'android/app/src/main/res/font/**'
- 'android/app/src/main/assets/licenses/**'
- 'crates/pstr-android/**'
- 'crates/pstr-core/**'
- 'crates/pstr-meta/**'
- 'crates/pstr-stream/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'scripts/build-ios.sh'
- '.github/workflows/ios.yml'
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Unit tests and unsigned .ipa
runs-on: macos-15
steps:
- uses: actions/checkout@v7
- id: toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.96'
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- name: Restore the Rust target tree
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: rust-target-ios-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('Cargo.lock') }}
restore-keys: rust-target-ios-${{ steps.toolchain.outputs.cachekey }}-
- name: Install XcodeGen
run: brew install xcodegen
- name: Bindings
run: scripts/build-ios.sh bindings
- name: Unit tests on the simulator
run: scripts/build-ios.sh test
# SideStore and AltStore re-sign whatever they install with the viewer's
# own Apple ID, so the release artifact is unsigned.
- name: Unsigned .ipa
run: |
scripts/build-ios.sh rust
scripts/build-ios.sh ipa
- uses: actions/upload-artifact@v7
with:
name: ios
path: dist/*.ipa
if-no-files-found: error