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:
@@ -57,6 +57,64 @@ function haptic(kind: 'tap'|'done'|'warn'){
|
||||
// Here, after ipxHost exists: in the app the button waits for the host to say it has a picker.
|
||||
paintRoute();
|
||||
|
||||
/// The app's own settings (#445): which server it opens, and the player token it sends instead of
|
||||
/// the page's cookie (#419). They were in an alert that opened on shaking the phone, which nobody
|
||||
/// found. The app sends them at ready and after each change; null in a browser, or in an app that
|
||||
/// keeps them elsewhere, and then Settings shows nothing for them.
|
||||
type AppSettings = {server: string, api: string, tokenSet: boolean, saved?: boolean};
|
||||
let appSettings: AppSettings|null=null;
|
||||
function hostAppSettings(m: AppSettings){
|
||||
appSettings=m;
|
||||
if(m.saved) toast('Saved on this device.');
|
||||
// Settings open: drawn again, so "Not set" turns to "Set" under the person's eyes.
|
||||
const box=$('#sdevice');
|
||||
if(box){ box.outerHTML=deviceSettingsHTML(); wireDeviceSettings(); }
|
||||
}
|
||||
|
||||
function deviceSettingsHTML(){
|
||||
const a=appSettings;
|
||||
if(!a) return '';
|
||||
return `<div class="field boxed" id="sdevice"><label>This device</label>
|
||||
<span class="hint">The iPX app's own settings, kept on this phone.</span>
|
||||
<label for="sserver">Server</label>
|
||||
<div class="inline"><input type="text" inputmode="url" id="sserver" value="${esc(a.server)}" autocapitalize="off" autocomplete="off" spellcheck="false">
|
||||
<button class="btn" id="sserverset">Open</button></div>
|
||||
<span class="hint">The address the app opens. Plain http only works on your own network, such as 192.168.1.130:8099.</span>
|
||||
<label for="sptoken">Player token</label>
|
||||
<div class="inline"><input type="password" id="sptoken" autocomplete="off" spellcheck="false"
|
||||
placeholder="${a.tokenSet?'Set':'Not set'}">
|
||||
<button class="btn" id="sptokenmake" title="Make a token for this device and save it" aria-label="Make a player token">${ICON.plus} Make</button>
|
||||
<button class="btn ico" id="sptokenclear" title="Clear the player token" aria-label="Clear the player token"${a.tokenSet?'':' hidden'}>${ICON.trash}</button></div>
|
||||
<label for="sapi">Player address</label>
|
||||
<div class="inline"><input type="text" inputmode="url" id="sapi" value="${esc(a.api)}" autocapitalize="off" autocomplete="off" spellcheck="false">
|
||||
<button class="btn" id="sptokensave">Save</button></div>
|
||||
<span class="hint">With a token, playback and CarPlay keep working when this page's sign-in
|
||||
ends. Make one here, or paste one from API tokens below. It is kept in the phone's keychain.</span></div>`;
|
||||
}
|
||||
|
||||
function wireDeviceSettings(){
|
||||
if(!appSettings||!ipxHost) return;
|
||||
const host=ipxHost;
|
||||
$<HTMLButtonElement>('#sserverset').onclick=()=>{
|
||||
const url=$<HTMLInputElement>('#sserver').value.trim();
|
||||
// The app checks it, and on a new server loads it in place of this page.
|
||||
if(url&&url!==appSettings?.server) host({t:'setServer', url});
|
||||
};
|
||||
const api_=()=>$<HTMLInputElement>('#sapi').value.trim();
|
||||
$<HTMLButtonElement>('#sptokensave').onclick=()=>{
|
||||
const token=$<HTMLInputElement>('#sptoken').value.trim();
|
||||
host({t:'setToken', api:api_(), ...(token?{token}:{})});
|
||||
};
|
||||
$<HTMLButtonElement>('#sptokenmake').onclick=async()=>{
|
||||
try{
|
||||
const t=await api('/api/tokens',{method:'POST',body:JSON.stringify({name:'iPX app on a phone'})});
|
||||
host({t:'setToken', api:api_(), token:t.token});
|
||||
drawTokens();
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
$<HTMLButtonElement>('#sptokenclear').onclick=()=>host({t:'clearToken'});
|
||||
}
|
||||
|
||||
function installNativePlayback(){
|
||||
const post = ipxHost!;
|
||||
const M = HTMLMediaElement.prototype;
|
||||
@@ -207,6 +265,8 @@ function installNativePlayback(){
|
||||
if(m.t==='host'){ hostCan.clear(); for(const c of m.can||[]) hostCan.add(c); paintRoute(); return; }
|
||||
if(m.t==='sleep'||m.t==='slept'){ hostSleep(m); return; }
|
||||
if(m.t==='push'){ hostPush(m); return; }
|
||||
if(m.t==='appSettings'){ hostAppSettings(m); return; }
|
||||
if(m.t==='appSettingsError'){ toast(m.message||'The app did not take that.', true); return; }
|
||||
if(m.t==='nowPlaying'){ adopt(m); return; }
|
||||
if(!N.on) return;
|
||||
switch(m.t){
|
||||
|
||||
Reference in New Issue
Block a user