iOS: the app's settings in Settings, not behind a shake; iPhone upright (#445)

The server address and the player token were set from alerts that
opened on shaking the phone, which nobody finds. Settings now has a This
device section in the app: the server, the player token and its
address, and a Make button that creates an API token and hands it
straight to the app's keychain. The app sends what it has at ready
(appSettings) and takes setServer, setToken and clearToken; a new
server rebuilds the web view as the alert did. The first-launch prompt
and the Cannot reach alert stay native, since there is no page then.

The iPhone is portrait only; the iPad keeps all four orientations, which
Split View and Slide Over need (#434 checked the iPad's layout).

Build 14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ray Slakinski
2026-11-13 08:39:54 -05:00
parent 69f1a38e9f
commit 171c77475a
10 changed files with 142 additions and 61 deletions

View File

@@ -45,7 +45,9 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
view.addSubview(webView)
cookies = CookieBridge(store: cfg.websiteDataStore.httpCookieStore)
cfg.userContentController.add(Bridge(webView: webView, playback: playback), name: Bridge.name)
let bridge = Bridge(webView: webView, playback: playback)
bridge.onServer = { [weak self] url in self?.open(url) }
cfg.userContentController.add(bridge, name: Bridge.name)
}
override func viewDidAppear(_ animated: Bool) {
@@ -57,16 +59,13 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
// MARK: - which server
/// Shake to change it. A setting touched about once, so it takes no room on the screen.
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake { askForServer() }
}
/// Asked on first launch, and when the server cannot be reached; otherwise changed in the
/// page's Settings (#445), which replaced shaking the phone for it.
private func askForServer(_ problem: String? = nil) {
guard presentedViewController == nil else { return }
let alert = UIAlertController(
title: "Your iPX server",
message: problem ?? "The address you open iPX at. Shake the phone to change it later.",
message: problem ?? "The address you open iPX at. You can change it later in Settings.",
preferredStyle: .alert)
alert.addTextField { f in
f.text = Server.url?.absoluteString ?? Server.suggested
@@ -76,9 +75,6 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
f.clearButtonMode = .whileEditing
}
if Server.url != nil { alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) }
alert.addAction(UIAlertAction(title: "Player Token…", style: .default) { [weak self] _ in
self?.askForCredentials()
})
alert.addAction(UIAlertAction(title: "Open", style: .default) { [weak self, weak alert] _ in
guard let self else { return }
let text = alert?.textFields?.first?.text ?? ""
@@ -87,51 +83,16 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
+ "on your own network, such as 192.168.1.130:8099.")
return
}
Server.url = url
self.playback.stop()
self.makeWebView()
self.webView.load(URLRequest(url: url))
self.open(url)
})
present(alert, animated: true)
}
/// The token the player and CarPlay use in place of the page's cookie, and the address they
/// send it to (#419). An empty token field keeps the one there is; Clear removes it.
private func askForCredentials(_ problem: String? = nil) {
guard presentedViewController == nil else { return }
let alert = UIAlertController(
title: "Player token",
message: problem ?? "An ipx API token (Settings, API tokens) so playback and CarPlay keep "
+ "working when the page's sign-in ends. "
+ (Credentials.isSet ? "Set now; leave the token empty to keep it." : "Not set."),
preferredStyle: .alert)
alert.addTextField { f in
f.placeholder = "ipx API token"
f.isSecureTextEntry = true
f.autocapitalizationType = .none
f.autocorrectionType = .no
}
alert.addTextField { f in
f.text = Server.apiURL?.absoluteString ?? Server.suggestedAPI
f.keyboardType = .URL
f.autocapitalizationType = .none
f.autocorrectionType = .no
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Clear", style: .destructive) { _ in
Credentials.apiToken = nil
})
alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ in
let fields = alert?.textFields ?? []
guard fields.count == 2, let url = Server.normalise(fields[1].text ?? ""),
url.scheme == "https" else {
self?.askForCredentials("The player address has to be https, such as \(Server.suggestedAPI).")
return
}
Server.apiURL = url
if let token = fields[0].text, !token.isEmpty { Credentials.apiToken = token }
})
present(alert, animated: true)
private func open(_ url: URL) {
Server.url = url
playback.stop()
makeWebView()
webView.load(URLRequest(url: url))
}
// MARK: - the page