mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
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.
89 lines
3.1 KiB
Swift
89 lines
3.1 KiB
Swift
import SwiftUI
|
|
|
|
/// The type ramp, which is the desktop client's type ramp, under Material's
|
|
/// names so a view ported from the Android client asks for the same style.
|
|
///
|
|
/// Inter, Regular and SemiBold only, as on the desktop and on Android: a
|
|
/// `Medium` role resolves to Regular. The rungs, largest first:
|
|
/// 26 / 20 / 18 / 17 / 15 / 14 / 13 / 12 / 11. `TypeRampTests` pins them.
|
|
enum TypeRole: CaseIterable {
|
|
case displayLarge, displayMedium, displaySmall
|
|
case headlineLarge, headlineMedium, headlineSmall
|
|
case titleLarge, titleMedium, titleSmall
|
|
case bodyLarge, bodyMedium, bodySmall
|
|
case labelLarge, labelMedium, labelSmall
|
|
|
|
var size: CGFloat {
|
|
switch self {
|
|
case .displayLarge, .displayMedium, .headlineLarge, .headlineMedium: Ramp.display
|
|
case .displaySmall, .titleLarge: Ramp.title
|
|
case .headlineSmall: Ramp.heading
|
|
case .titleMedium: Ramp.section
|
|
case .titleSmall: Ramp.subhead
|
|
case .bodyLarge: Ramp.body
|
|
case .bodyMedium, .labelLarge: Ramp.label
|
|
case .bodySmall, .labelMedium: Ramp.caption
|
|
case .labelSmall: Ramp.micro
|
|
}
|
|
}
|
|
|
|
var semibold: Bool {
|
|
switch self {
|
|
case .displayLarge, .displayMedium, .displaySmall, .headlineLarge, .headlineMedium, .headlineSmall, .titleLarge:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The Dynamic Type style each rung scales with.
|
|
var relative: Font.TextStyle {
|
|
switch size {
|
|
case Ramp.display: .title
|
|
case Ramp.title: .title2
|
|
case Ramp.heading: .title3
|
|
case Ramp.section: .headline
|
|
case Ramp.subhead: .subheadline
|
|
case Ramp.body: .body
|
|
case Ramp.label: .callout
|
|
case Ramp.caption: .footnote
|
|
default: .caption
|
|
}
|
|
}
|
|
|
|
/// Line height is derived rather than named per style: 1.35 of the size,
|
|
/// as on Android. Inter's own leading is about 1.21, so the rest is spacing.
|
|
var lineSpacing: CGFloat { size * (Ramp.lineHeight - 1.21) }
|
|
}
|
|
|
|
enum Ramp {
|
|
static let display: CGFloat = 26
|
|
static let title: CGFloat = 20
|
|
static let heading: CGFloat = 18
|
|
static let section: CGFloat = 17
|
|
static let subhead: CGFloat = 15
|
|
static let body: CGFloat = 14
|
|
static let label: CGFloat = 13
|
|
static let caption: CGFloat = 12
|
|
static let micro: CGFloat = 11
|
|
static let lineHeight: CGFloat = 1.35
|
|
}
|
|
|
|
extension Font {
|
|
static func inter(_ style: TypeRole, semibold: Bool? = nil) -> Font {
|
|
.custom((semibold ?? style.semibold) ? "Inter-SemiBold" : "Inter-Regular", size: style.size, relativeTo: style.relative)
|
|
}
|
|
|
|
static func inter(size: CGFloat, semibold: Bool = false) -> Font {
|
|
.custom(semibold ? "Inter-SemiBold" : "Inter-Regular", size: size)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// One rung of the ramp. `semibold` overrides the style's own weight, the
|
|
/// counterpart of Compose's `fontWeight = FontWeight.SemiBold`.
|
|
func textStyle(_ style: TypeRole, semibold: Bool? = nil) -> some View {
|
|
font(.inter(style, semibold: semibold)).lineSpacing(style.lineSpacing)
|
|
}
|
|
}
|