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

@@ -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
}
}