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

+34 -10
View File
@@ -11,14 +11,16 @@ repo_root="$(cd -- "$script_dir/.." && pwd)"
ios_dir="$repo_root/ios"
generated="$ios_dir/build/generated"
target=aarch64-apple-ios
simulator=aarch64-apple-ios-sim
usage() {
echo "usage: $0 {rust|bindings|ipa|all}"
echo "usage: $0 {rust|bindings|ipa|all|test}"
echo
echo " rust static libpstr_android.a for $target"
echo " bindings Swift bindings into ios/build/generated"
echo " ipa xcodegen + xcodebuild, then dist/ProtonStream-<version>.ipa"
echo " ipa xcodegen + xcodebuild, then dist/proton-stream-<version>-ios.ipa"
echo " all all three, in that order"
echo " test the unit tests on a simulator ($simulator)"
}
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
@@ -36,11 +38,19 @@ version="$(awk '/^\[workspace.package\]/{found=1; next} found && /^\[/{exit} fou
[[ -n "$version" ]] || die "no [workspace.package] version in Cargo.toml"
build_rust() {
log "cargo rustc -p pstr-android --target $target (staticlib)"
local triple="${1:-$target}"
log "cargo rustc -p pstr-android --target $triple (staticlib)"
# The crate stays `cdylib, rlib` for Android; the archive iOS links is asked
# for here instead of being built on every other platform too.
( cd "$repo_root" && IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo rustc -p pstr-android --lib \
--release --locked --target "$target" --crate-type staticlib )
--release --locked --target "$triple" --crate-type staticlib )
}
generate_project() {
command -v xcodegen >/dev/null || die "xcodegen is required (brew install xcodegen)"
[[ -f "$generated/PstrBridge.swift" ]] || die "run '$0 bindings' first"
log "xcodegen ($version)"
( cd "$ios_dir" && PSTR_VERSION="$version" PSTR_BUILD="${PSTR_BUILD:-1}" xcodegen generate --quiet )
}
build_bindings() {
@@ -56,12 +66,8 @@ build_bindings() {
}
build_ipa() {
command -v xcodegen >/dev/null || die "xcodegen is required (brew install xcodegen)"
[[ -f "$repo_root/target/$target/release/libpstr_android.a" ]] || die "run '$0 rust' first"
[[ -f "$generated/PstrBridge.swift" ]] || die "run '$0 bindings' first"
log "xcodegen ($version)"
( cd "$ios_dir" && PSTR_VERSION="$version" PSTR_BUILD="${PSTR_BUILD:-1}" xcodegen generate --quiet )
generate_project
log "xcodebuild (unsigned)"
xcodebuild -project "$ios_dir/ProtonStream.xcodeproj" -scheme ProtonStream \
@@ -76,16 +82,34 @@ build_ipa() {
rm -rf "$stage"
mkdir -p "$stage/Payload" "$repo_root/dist"
cp -R "$app" "$stage/Payload/"
local ipa="$repo_root/dist/ProtonStream-$version.ipa"
local ipa="$repo_root/dist/proton-stream-$version-ios.ipa"
rm -f "$ipa"
( cd "$stage" && zip -qry "$ipa" Payload )
log "wrote $ipa"
}
run_tests() {
build_rust "$simulator"
[[ -f "$generated/PstrBridge.swift" ]] || build_bindings
generate_project
# Whichever iPhone this Xcode ships a simulator for: the names change with
# every release, and the tests do not care which one.
local device
device="$(xcrun simctl list devices available | grep -m1 -oE 'iPhone [^(]+' | sed 's/ *$//')"
[[ -n "$device" ]] || die "no iPhone simulator is installed"
log "xcodebuild test ($device)"
xcodebuild -project "$ios_dir/ProtonStream.xcodeproj" -scheme ProtonStream \
-destination "platform=iOS Simulator,name=$device" \
-derivedDataPath "$ios_dir/build/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
test
}
case "${1:-}" in
rust) build_rust ;;
bindings) build_bindings ;;
ipa) build_ipa ;;
all) build_rust; build_bindings; build_ipa ;;
test) run_tests ;;
*) usage >&2; exit 2 ;;
esac