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.
41 lines
1.8 KiB
Swift
41 lines
1.8 KiB
Swift
import Foundation
|
|
|
|
/// A Proton Drive public link, wherever in a piece of text it sits.
|
|
///
|
|
/// The fragment is kept: it is the share's secret, and a link without it cannot
|
|
/// be opened. Matching stops at whitespace and at the quotes and brackets a
|
|
/// message app wraps a link in, which never occur in a link itself.
|
|
private let shareLinkPattern = #"https://drive\.proton\.me/urls/[^\s"'<>()\[\]]+"#
|
|
|
|
/// The first share link in `text`, if any — for the clipboard.
|
|
func shareLink(in text: String) -> String? {
|
|
text.range(of: shareLinkPattern, options: .regularExpression).map { String(text[$0]) }
|
|
}
|
|
|
|
/// The share link a URL opened in this app carries, if any.
|
|
///
|
|
/// iOS hands an app no `https://drive.proton.me` link — Universal Links belong
|
|
/// to the domain's owner — so a link arrives through the app's own scheme:
|
|
/// `protonstream://add?url=<link>`, or the link itself with its scheme swapped,
|
|
/// `protonstream://drive.proton.me/urls/…#…`.
|
|
func shareLink(from url: URL) -> String? {
|
|
guard url.scheme?.lowercased() == "protonstream" else { return nil }
|
|
if url.host == "add" {
|
|
let value = URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
|
.queryItems?.first { $0.name == "url" }?.value
|
|
return value.flatMap(wholeShareLink)
|
|
}
|
|
let text = url.absoluteString
|
|
let rest = text[text.index(text.startIndex, offsetBy: "protonstream://".count)...]
|
|
return wholeShareLink("https://\(rest)")
|
|
}
|
|
|
|
/// `value` when the whole of it is a share link — what `ACTION_VIEW` accepts on
|
|
/// Android — rather than a link somewhere inside it.
|
|
private func wholeShareLink(_ value: String) -> String? {
|
|
guard let range = value.range(of: shareLinkPattern, options: .regularExpression),
|
|
range == value.startIndex ..< value.endIndex
|
|
else { return nil }
|
|
return value
|
|
}
|