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:
@@ -62,6 +62,37 @@ final class Bridge: NSObject, WKScriptMessageHandler {
|
||||
"keeping": Offline.shared.downloading.sorted()])
|
||||
}
|
||||
|
||||
/// A new server, checked, for the web view to load in place of this page (#445).
|
||||
var onServer: ((URL) -> Void)?
|
||||
|
||||
/// The app's own settings for the page's Settings to show (#445); `saved` says a change took.
|
||||
private func sendAppSettings(saved: Bool = false) {
|
||||
send(["t": "appSettings", "server": Server.url?.absoluteString ?? Server.suggested,
|
||||
"api": Server.apiURL?.absoluteString ?? Server.suggestedAPI,
|
||||
"tokenSet": Credentials.apiToken != nil, "saved": saved])
|
||||
}
|
||||
|
||||
/// The page's Settings, setting what an alert on shaking the phone used to (#445).
|
||||
private func setToken(_ m: [String: Any]) {
|
||||
guard let url = Server.normalise(m["api"] as? String ?? ""), url.scheme == "https" else {
|
||||
send(["t": "appSettingsError",
|
||||
"message": "The player address has to be https, such as \(Server.suggestedAPI)."])
|
||||
return
|
||||
}
|
||||
Server.apiURL = url
|
||||
if let token = m["token"] as? String, !token.isEmpty { Credentials.apiToken = token }
|
||||
sendAppSettings(saved: true)
|
||||
}
|
||||
|
||||
private func setServer(_ text: String) {
|
||||
guard let url = Server.normalise(text) else {
|
||||
send(["t": "appSettingsError", "message": "That is not an address the app can open. Plain http "
|
||||
+ "only works on your own network, such as 192.168.1.130:8099."])
|
||||
return
|
||||
}
|
||||
onServer?(url)
|
||||
}
|
||||
|
||||
/// What is loaded, for the page's player bar to take up (#442): the car's pick, or an episode
|
||||
/// playing when the page loads, as when the app was opened from CarPlay. Without it the sound
|
||||
/// went on with no bar on the phone to pause it.
|
||||
@@ -91,7 +122,8 @@ final class Bridge: NSObject, WKScriptMessageHandler {
|
||||
if let r = m["rate"] as? Double { playback.setRate(Float(r)) }
|
||||
if let v = m["volume"] as? Double { playback.setVolume(Float(v)) }
|
||||
// What this build can do, so a newer page offers nothing it would ask for in vain.
|
||||
send(["t": "host", "can": ["sleep", "route"]])
|
||||
send(["t": "host", "can": ["sleep", "route", "settings"]])
|
||||
sendAppSettings()
|
||||
// A page reloaded while the timer runs is told how long is left.
|
||||
if let at = playback.sleepAt { send(["t": "sleep", "left": max(0, at.timeIntervalSinceNow)]) }
|
||||
sendKept()
|
||||
@@ -123,6 +155,11 @@ final class Bridge: NSObject, WKScriptMessageHandler {
|
||||
case "route": showRoutes(m)
|
||||
case "haptic": haptic(m["kind"] as? String)
|
||||
case "unread": if let n = m["n"] as? Int { Badge.set(n) }
|
||||
case "setServer": setServer(m["url"] as? String ?? "")
|
||||
case "setToken": setToken(m)
|
||||
case "clearToken":
|
||||
Credentials.apiToken = nil
|
||||
sendAppSettings(saved: true)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// Which ipx-web the app shows. Asked for on first launch; shaking the phone asks again.
|
||||
/// Which ipx-web the app shows. Asked for on first launch; changed in the page's Settings (#445).
|
||||
/// `-ipx.server <url>` on the launch arguments overrides it for that run (UserDefaults' argument
|
||||
/// domain), which is how a test points the app somewhere else.
|
||||
enum Server {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user