release(v1.8.3): feat(mobile+ci): harden Capacitor switcher & make release-on-version robust
This commit is contained in:
28
.github/workflows/release-on-version.yml
vendored
28
.github/workflows/release-on-version.yml
vendored
@@ -17,26 +17,39 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref }}-${{ github.sha }}
|
# Cancel older runs for the same branch/ref so only the latest proceeds
|
||||||
cancel-in-progress: false
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout correct ref
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
# For workflow_run, use the triggering workflow's head_sha; else use the current SHA
|
||||||
|
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
- name: Ensure tags available
|
- name: Ensure tags available
|
||||||
run: |
|
run: |
|
||||||
git fetch --tags --force --prune --quiet
|
git fetch --tags --force --prune --quiet
|
||||||
|
|
||||||
|
- name: Show recent tags (debug)
|
||||||
|
run: git tag --list "v*" --sort=-v:refname | head -n 20
|
||||||
|
|
||||||
- name: Read version from version.js
|
- name: Read version from version.js
|
||||||
id: ver
|
id: ver
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
VER=$(grep -Eo "APP_VERSION\s*=\s*['\"]v[^'\"]+['\"]" public/js/version.js | sed -E "s/.*['\"](v[^'\"]+)['\"].*/\1/")
|
echo "version.js at commit: $(git rev-parse --short HEAD)"
|
||||||
if [[ -z "$VER" ]]; then
|
sed -n '1,80p' public/js/version.js || true
|
||||||
|
|
||||||
|
VER=$(
|
||||||
|
grep -Eo "APP_VERSION[^\\n]*['\"]v[0-9][^'\"]+['\"]" public/js/version.js \
|
||||||
|
| sed -E "s/.*['\"](v[^'\"]+)['\"].*/\1/" \
|
||||||
|
| tail -n1
|
||||||
|
)
|
||||||
|
if [[ -z "${VER:-}" ]]; then
|
||||||
echo "Could not parse APP_VERSION from version.js" >&2
|
echo "Could not parse APP_VERSION from version.js" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -69,7 +82,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
VER="${{ steps.ver.outputs.version }}" # e.g. v1.6.12
|
VER="${{ steps.ver.outputs.version }}" # e.g. v1.8.2
|
||||||
ZIP="FileRise-${VER}.zip"
|
ZIP="FileRise-${VER}.zip"
|
||||||
|
|
||||||
# Clean staging copy (exclude dotfiles you don’t want)
|
# Clean staging copy (exclude dotfiles you don’t want)
|
||||||
@@ -195,7 +208,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.ver.outputs.version }}
|
tag_name: ${{ steps.ver.outputs.version }}
|
||||||
target_commitish: ${{ github.sha }}
|
# Point the tag at the same commit we checked out (handles workflow_run case)
|
||||||
|
target_commitish: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||||
name: ${{ steps.ver.outputs.version }}
|
name: ${{ steps.ver.outputs.version }}
|
||||||
body_path: RELEASE_BODY.md
|
body_path: RELEASE_BODY.md
|
||||||
generate_release_notes: false
|
generate_release_notes: false
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changees 11/4/2025 (v1.8.3)
|
||||||
|
|
||||||
|
release(v1.8.3): feat(mobile+ci): harden Capacitor switcher & make release-on-version robust
|
||||||
|
|
||||||
|
- switcher.js: allow running inside Capacitor; remove innerHTML usage; build nodes safely; normalize/strip creds from URLs; add withParam() for ?frapp=1; drop inline handlers; clamp rename length; minor UX polish.
|
||||||
|
- CI: cancel superseded runs per ref; checkout triggering commit (workflow_run head_sha); improve APP_VERSION parsing; point tag to checked-out commit; add recent-tag debug.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 11/4/2025 (v1.8.2)
|
## Changes 11/4/2025 (v1.8.2)
|
||||||
|
|
||||||
release(v1.8.2): media progress tracking + watched badges; PWA scaffolding; mobile switcher (closes #37)
|
release(v1.8.2): media progress tracking + watched badges; PWA scaffolding; mobile switcher (closes #37)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(function(){
|
(function(){
|
||||||
const isCap = !!window.Capacitor || /Capacitor/i.test(navigator.userAgent);
|
const isCap = !!window.Capacitor || /Capacitor/i.test(navigator.userAgent);
|
||||||
if (!isCap) return;
|
if (!isCap) return;
|
||||||
if ((location.origin || '').startsWith('capacitor://')) return;
|
// NOTE: allow running inside Capacitor (origin "capacitor://localhost")
|
||||||
|
|
||||||
const Plugins = (window.Capacitor && window.Capacitor.Plugins) || {};
|
const Plugins = (window.Capacitor && window.Capacitor.Plugins) || {};
|
||||||
const Pref = Plugins.Preferences ? {
|
const Pref = Plugins.Preferences ? {
|
||||||
@@ -18,11 +18,51 @@
|
|||||||
const K_INST='fr_instances_v1', K_ACTIVE='fr_active_v1', K_STATUS='fr_status_v1';
|
const K_INST='fr_instances_v1', K_ACTIVE='fr_active_v1', K_STATUS='fr_status_v1';
|
||||||
|
|
||||||
const $ = s => document.querySelector(s);
|
const $ = s => document.querySelector(s);
|
||||||
const el = (t,a={},html='') => { const n=document.createElement(t); for (const k in a) n.setAttribute(k,a[k]); n.innerHTML=html; return n; };
|
|
||||||
const normalize = u => { if(!u) return ''; let v=u.trim(); if(!/^https?:\/\//i.test(v)) v='https://'+v; return v.replace(/\/+$/,''); };
|
// Safe element builder: attributes only, children as nodes/strings (no innerHTML)
|
||||||
const host = u => { try{ return new URL(normalize(u)).hostname }catch{ return '' } };
|
const el = (tag, attrs = {}, children = []) => {
|
||||||
const originOf = u => { try{ return new URL(normalize(u)).origin }catch{ return '' } };
|
const n = document.createElement(tag);
|
||||||
const faviconUrl = u => { try{ const x=new URL(normalize(u)); return x.origin+'/favicon.ico' }catch{ return '' } };
|
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
||||||
|
(Array.isArray(children) ? children : [children]).forEach(c => {
|
||||||
|
if (c == null) return;
|
||||||
|
n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||||
|
});
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize to http(s), strip creds, collapse trailing slashes
|
||||||
|
const normalize = (u) => {
|
||||||
|
if (!u) return '';
|
||||||
|
let v = u.trim();
|
||||||
|
if (!/^https?:\/\//i.test(v)) v = 'https://' + v;
|
||||||
|
try {
|
||||||
|
const url = new URL(v);
|
||||||
|
if (!/^https?:$/.test(url.protocol)) return '';
|
||||||
|
url.username = '';
|
||||||
|
url.password = '';
|
||||||
|
url.pathname = url.pathname.replace(/\/+$/,'');
|
||||||
|
return url.toString();
|
||||||
|
} catch { return ''; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Append/overwrite a query param safely on a normalized URL
|
||||||
|
const withParam = (base, k, v) => {
|
||||||
|
try {
|
||||||
|
const u = new URL(normalize(base));
|
||||||
|
u.searchParams.set(k, v);
|
||||||
|
return u.toString();
|
||||||
|
} catch { return ''; }
|
||||||
|
};
|
||||||
|
|
||||||
|
const host = u => {
|
||||||
|
try { return new URL(normalize(u)).hostname; } catch { return ''; }
|
||||||
|
};
|
||||||
|
const originOf = u => {
|
||||||
|
try { return new URL(normalize(u)).origin; } catch { return ''; }
|
||||||
|
};
|
||||||
|
const faviconUrl = u => {
|
||||||
|
try { const x = new URL(normalize(u)); return x.origin + '/favicon.ico'; } catch { return ''; }
|
||||||
|
};
|
||||||
const initialsIcon = (hn='FR') => {
|
const initialsIcon = (hn='FR') => {
|
||||||
const t=(hn||'FR').replace(/^www\./,'').slice(0,2).toUpperCase();
|
const t=(hn||'FR').replace(/^www\./,'').slice(0,2).toUpperCase();
|
||||||
const svg=`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'>
|
const svg=`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'>
|
||||||
@@ -32,12 +72,19 @@
|
|||||||
return 'data:image/svg+xml;utf8,'+encodeURIComponent(svg);
|
return 'data:image/svg+xml;utf8,'+encodeURIComponent(svg);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getStatusCache(){ const raw=(await Pref.get({key:K_STATUS})).value; try{ return raw?JSON.parse(raw):{} }catch{ return {}; } }
|
async function getStatusCache(){
|
||||||
async function writeStatus(origin, ok){ const cache=await getStatusCache(); cache[origin]={ ok, ts: Date.now() }; await Pref.set({key:K_STATUS, value:JSON.stringify(cache)}); }
|
const raw=(await Pref.get({key:K_STATUS})).value;
|
||||||
|
try { return raw ? JSON.parse(raw) : {}; } catch { return {}; }
|
||||||
|
}
|
||||||
|
async function writeStatus(origin, ok){
|
||||||
|
const cache=await getStatusCache();
|
||||||
|
cache[origin]={ ok, ts: Date.now() };
|
||||||
|
await Pref.set({key:K_STATUS, value:JSON.stringify(cache)});
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyFileRise(u, timeout=5000){
|
async function verifyFileRise(u, timeout=5000){
|
||||||
if (!u || !Http) return {ok:false};
|
if (!u || !Http) return {ok:false};
|
||||||
const base = normalize(u), origin = originOf(base);
|
const base = normalize(u), org = originOf(base);
|
||||||
const tryJson = async (url, validate) => {
|
const tryJson = async (url, validate) => {
|
||||||
try{
|
try{
|
||||||
const r = await Http.get({ url, connectTimeout:timeout, readTimeout:timeout, headers:{'Accept':'application/json','Cache-Control':'no-cache'} });
|
const r = await Http.get({ url, connectTimeout:timeout, readTimeout:timeout, headers:{'Accept':'application/json','Cache-Control':'no-cache'} });
|
||||||
@@ -48,14 +95,14 @@
|
|||||||
}catch(_){}
|
}catch(_){}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if (await tryJson(origin + '/siteConfig.json', j => j && (j.appTitle || j.headerTitle || j.auth || j.oidc || j.basicAuth))) return {ok:true, origin};
|
if (await tryJson(org + '/siteConfig.json', j => j && (j.appTitle || j.headerTitle || j.auth || j.oidc || j.basicAuth))) return {ok:true, origin:org};
|
||||||
if (await tryJson(origin + '/api/ping.php', j => j && (j.ok===true || j.status==='ok' || j.pong || j.app==='FileRise'))) return {ok:true, origin};
|
if (await tryJson(org + '/api/ping.php', j => j && (j.ok===true || j.status==='ok' || j.pong || j.app==='FileRise'))) return {ok:true, origin:org};
|
||||||
if (await tryJson(origin + '/api/version.php', j => j && (j.version || j.app==='FileRise'))) return {ok:true, origin};
|
if (await tryJson(org + '/api/version.php', j => j && (j.version || j.app==='FileRise'))) return {ok:true, origin:org};
|
||||||
try{
|
try{
|
||||||
const r = await Http.get({ url: origin+'/', connectTimeout:timeout, readTimeout:timeout, headers:{'Cache-Control':'no-cache'} });
|
const r = await Http.get({ url: org+'/', connectTimeout:timeout, readTimeout:timeout, headers:{'Cache-Control':'no-cache'} });
|
||||||
if (typeof r.data === 'string' && /FileRise/i.test(r.data)) return {ok:true, origin};
|
if (typeof r.data === 'string' && /FileRise/i.test(r.data)) return {ok:true, origin:org};
|
||||||
}catch(_){}
|
}catch(_){}
|
||||||
return {ok:false, origin};
|
return {ok:false, origin:org};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function probeReachable(u, timeout=3000){
|
async function probeReachable(u, timeout=3000){
|
||||||
@@ -77,8 +124,13 @@
|
|||||||
}catch{ return false; }
|
}catch{ return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadInstances(){ const raw=(await Pref.get({key:K_INST})).value; try{ return raw?JSON.parse(raw):[] }catch{ return [] } }
|
async function loadInstances(){
|
||||||
async function saveInstances(list){ await Pref.set({key:K_INST, value:JSON.stringify(list)}); }
|
const raw=(await Pref.get({key:K_INST})).value;
|
||||||
|
try { return raw ? JSON.parse(raw) : []; } catch { return []; }
|
||||||
|
}
|
||||||
|
async function saveInstances(list){
|
||||||
|
await Pref.set({key:K_INST, value:JSON.stringify(list)});
|
||||||
|
}
|
||||||
async function getActive(){ return (await Pref.get({key:K_ACTIVE})).value }
|
async function getActive(){ return (await Pref.get({key:K_ACTIVE})).value }
|
||||||
async function setActive(id){ await Pref.set({key:K_ACTIVE, value:id||''}) }
|
async function setActive(id){ await Pref.set({key:K_ACTIVE, value:id||''}) }
|
||||||
|
|
||||||
@@ -128,32 +180,41 @@
|
|||||||
document.head.appendChild(el('style',{id:'frx-mobile-style'}, css));
|
document.head.appendChild(el('style',{id:'frx-mobile-style'}, css));
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOM
|
// ---- DOM skeleton (no innerHTML) ----
|
||||||
const scrim = el('div',{class:'frx-scrim', id:'frx-scrim'});
|
const scrim = el('div',{class:'frx-scrim', id:'frx-scrim'});
|
||||||
const sheet = el('div',{class:'frx-sheet', id:'frx-sheet'}, `
|
const sheet = el('div',{class:'frx-sheet', id:'frx-sheet'});
|
||||||
<div class="hdr">
|
const hdr = el('div',{class:'hdr'});
|
||||||
<div class="frx-title">
|
const title = el('div',{class:'frx-title'});
|
||||||
<img src="/assets/logo.svg" alt="FileRise" onerror="this.style.display='none'"><span>FileRise Switcher</span>
|
const logo = el('img',{src:'/assets/logo.svg', alt:'FileRise'});
|
||||||
</div>
|
// inline handler via property, not attribute
|
||||||
<div class="frx-row">
|
logo.onerror = function(){ this.style.display='none'; };
|
||||||
<button class="frx-btn frx-ghost" id="frx-home">Home</button>
|
title.append(logo, el('span',{},'FileRise Switcher'));
|
||||||
<button class="frx-btn frx-ghost" id="frx-close">Close</button>
|
const hdrBtns = el('div',{class:'frx-row'},[
|
||||||
</div>
|
el('button',{class:'frx-btn frx-ghost', id:'frx-home'},'Home'),
|
||||||
</div>
|
el('button',{class:'frx-btn frx-ghost', id:'frx-close'},'Close')
|
||||||
<div class="frx-list" id="frx-list"></div>
|
]);
|
||||||
<div style="padding:10px 12px">
|
hdr.append(title, hdrBtns);
|
||||||
<div class="frx-field">
|
|
||||||
<input class="frx-input" id="frx-name" placeholder="Display name (optional)"/>
|
const list = el('div',{class:'frx-list', id:'frx-list'});
|
||||||
<input class="frx-input" id="frx-url" placeholder="https://files.example.com"/>
|
const formWrap = el('div',{style:'padding:10px 12px'},[
|
||||||
</div>
|
el('div',{class:'frx-field'},[
|
||||||
</div>
|
el('input',{class:'frx-input', id:'frx-name', placeholder:'Display name (optional)'}),
|
||||||
<div class="frx-footer">
|
el('input',{class:'frx-input', id:'frx-url', placeholder:'https://files.example.com'})
|
||||||
<button class="frx-btn frx-ghost" id="frx-add-cancel">Close</button>
|
])
|
||||||
<button class="frx-btn frx-primary" id="frx-add-save">+ Add server</button>
|
]);
|
||||||
</div>
|
const footer = el('div',{class:'frx-footer'},[
|
||||||
`);
|
el('button',{class:'frx-btn frx-ghost', id:'frx-add-cancel'},'Close'),
|
||||||
const fab = el('div',{class:'frx-fab', id:'frx-fab', title:'Switch server'}, `<svg viewBox="0 0 24 24"><path d="M7 7h10v2H7V7zm0 4h10v2H7v-2zm0 4h10v2H7v-2z"/></svg>`);
|
el('button',{class:'frx-btn frx-primary', id:'frx-add-save'},'+ Add server')
|
||||||
document.body.appendChild(scrim); document.body.appendChild(sheet); document.body.appendChild(fab);
|
]);
|
||||||
|
sheet.append(hdr, list, formWrap, footer);
|
||||||
|
|
||||||
|
const fab = el('div',{class:'frx-fab', id:'frx-fab', title:'Switch server'},[
|
||||||
|
el('svg',{viewBox:'0 0 24 24'},[ el('path',{d:'M7 7h10v2H7V7zm0 4h10v2H7v-2zm0 4h10v2H7v-2z'}) ])
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.body.appendChild(scrim);
|
||||||
|
document.body.appendChild(sheet);
|
||||||
|
document.body.appendChild(fab);
|
||||||
|
|
||||||
function show(){ scrim.classList.add('show'); sheet.classList.add('show'); fab.style.display='none'; }
|
function show(){ scrim.classList.add('show'); sheet.classList.add('show'); fab.style.display='none'; }
|
||||||
function hide(){ scrim.classList.remove('show'); sheet.classList.remove('show'); fab.style.display='grid'; }
|
function hide(){ scrim.classList.remove('show'); sheet.classList.remove('show'); fab.style.display='grid'; }
|
||||||
@@ -164,37 +225,54 @@
|
|||||||
document.addEventListener('keydown', e=>{ if(e.key==='Escape') hide(); });
|
document.addEventListener('keydown', e=>{ if(e.key==='Escape') hide(); });
|
||||||
|
|
||||||
function chipNode(item, isActive){
|
function chipNode(item, isActive){
|
||||||
const hv=host(item.url);
|
const hv = host(item.url);
|
||||||
const node = el('div',{class:'frx-chip'+(isActive?' active':''), 'data-id':item.id});
|
const node = el('div',{class:'frx-chip'+(isActive?' active':''), 'data-id':item.id});
|
||||||
|
|
||||||
const top = el('div',{class:'frx-top'});
|
const top = el('div',{class:'frx-top'});
|
||||||
const left = el('div',{class:'frx-left'});
|
const left = el('div',{class:'frx-left'});
|
||||||
|
|
||||||
const ico = el('div',{class:'frx-ico'});
|
const ico = el('div',{class:'frx-ico'});
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.alt=''; img.src=item.favicon||faviconUrl(item.url)||initialsIcon(hv);
|
img.alt=''; img.src=item.favicon||faviconUrl(item.url)||initialsIcon(hv);
|
||||||
img.onerror=()=>{ img.onerror=null; img.src=initialsIcon(hv); };
|
img.onerror=()=>{ img.onerror=null; img.src=initialsIcon(hv); };
|
||||||
ico.appendChild(img);
|
ico.appendChild(img);
|
||||||
const txt = el('div',{}, `<div class="frx-name">${item.name || hv}</div><div class="frx-host">${hv}</div>`);
|
|
||||||
left.appendChild(ico); left.appendChild(txt);
|
const txt = el('div',{},[
|
||||||
const status = el('div',{class:'frx-status'}, `<span class="frx-dot" id="frx-dot-${item.id}"></span><span id="frx-lbl-${item.id}">Checking…</span>`);
|
el('div',{class:'frx-name'}, (item.name || hv)),
|
||||||
top.appendChild(left); top.appendChild(status);
|
el('div',{class:'frx-host'}, hv)
|
||||||
|
]);
|
||||||
|
|
||||||
|
left.appendChild(ico);
|
||||||
|
left.appendChild(txt);
|
||||||
|
|
||||||
|
const dot = el('span',{class:'frx-dot', id:`frx-dot-${item.id}`});
|
||||||
|
const lbl = el('span',{id:`frx-lbl-${item.id}`}, 'Checking…');
|
||||||
|
const status = el('div',{class:'frx-status'}, [dot, lbl]);
|
||||||
|
|
||||||
|
top.appendChild(left);
|
||||||
|
top.appendChild(status);
|
||||||
|
|
||||||
const actions = el('div',{class:'frx-actions'});
|
const actions = el('div',{class:'frx-actions'});
|
||||||
const bOpen = el('button',{class:'frx-btn frx-primary', 'data-act':'open', disabled:true}, 'Open');
|
const bOpen = el('button',{class:'frx-btn frx-primary', 'data-act':'open', disabled:true}, 'Open');
|
||||||
const bRen = el('button',{class:'frx-btn frx-ghost', 'data-act':'rename'}, 'Rename');
|
const bRen = el('button',{class:'frx-btn frx-ghost', 'data-act':'rename'}, 'Rename');
|
||||||
const bDel = el('button',{class:'frx-btn frx-danger', 'data-act':'remove'}, 'Remove');
|
const bDel = el('button',{class:'frx-btn frx-danger', 'data-act':'remove'}, 'Remove');
|
||||||
actions.appendChild(bOpen); actions.appendChild(bRen); actions.appendChild(bDel);
|
actions.appendChild(bOpen); actions.appendChild(bRen); actions.appendChild(bDel);
|
||||||
node.appendChild(top); node.appendChild(actions);
|
|
||||||
|
node.appendChild(top);
|
||||||
|
node.appendChild(actions);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderList(){
|
async function renderList(){
|
||||||
const listEl=$('#frx-list'); listEl.innerHTML='';
|
const listEl=$('#frx-list'); listEl.textContent='';
|
||||||
const list=await loadInstances(); const active=await getActive();
|
const list=await loadInstances(); const active=await getActive();
|
||||||
const cache=await getStatusCache();
|
const cache=await getStatusCache();
|
||||||
|
|
||||||
list.sort((a,b)=>(b.lastUsed||0)-(a.lastUsed||0)).forEach(item=>{
|
list.sort((a,b)=>(b.lastUsed||0)-(a.lastUsed||0)).forEach(item=>{
|
||||||
const chip = chipNode(item, item.id===active);
|
const chip = chipNode(item, item.id===active);
|
||||||
const o = originOf(item.url), cached = cache[o];
|
const o = originOf(item.url), cached = cache[o];
|
||||||
const dot = chip.querySelector(`#frx-dot-${item.id}`), lbl = chip.querySelector(`#frx-lbl-${item.id}`);
|
const dot = chip.querySelector(`#frx-dot-${item.id}`);
|
||||||
|
const lbl = chip.querySelector(`#frx-lbl-${item.id}`);
|
||||||
const openBtn = chip.querySelector('[data-act="open"]');
|
const openBtn = chip.querySelector('[data-act="open"]');
|
||||||
|
|
||||||
if (cached){
|
if (cached){
|
||||||
@@ -213,13 +291,13 @@
|
|||||||
if (act==='open'){
|
if (act==='open'){
|
||||||
if (openBtn.disabled) return;
|
if (openBtn.disabled) return;
|
||||||
await setActive(item.id);
|
await setActive(item.id);
|
||||||
const url=normalize(item.url), withFlag=url+(url.includes('?')?'&':'?')+'frapp=1';
|
const dest = withParam(item.url, 'frapp', '1');
|
||||||
window.location.replace(withFlag);
|
if (dest) window.location.replace(dest);
|
||||||
} else if (act==='rename'){
|
} else if (act==='rename'){
|
||||||
const nn=prompt('New display name:', item.name || host(item.url));
|
const nn=prompt('New display name:', item.name || host(item.url));
|
||||||
if (nn!=null){
|
if (nn!=null){
|
||||||
const L=await loadInstances(); const it=L.find(x=>x.id===item.id);
|
const L=await loadInstances(); const it=L.find(x=>x.id===item.id);
|
||||||
if (it){ it.name=nn.trim(); it.lastUsed=Date.now(); await saveInstances(L); renderList(); }
|
if (it){ it.name=nn.trim().slice(0,120); it.lastUsed=Date.now(); await saveInstances(L); renderList(); }
|
||||||
}
|
}
|
||||||
} else if (act==='remove'){
|
} else if (act==='remove'){
|
||||||
if (!confirm('Remove this server?')) return;
|
if (!confirm('Remove this server?')) return;
|
||||||
@@ -270,12 +348,12 @@
|
|||||||
|
|
||||||
if (vf.origin) await writeStatus(vf.origin, true);
|
if (vf.origin) await writeStatus(vf.origin, true);
|
||||||
|
|
||||||
window.location.replace(inst.url + (inst.url.includes('?')?'&':'?') + 'frapp=1');
|
const dest = withParam(inst.url, 'frapp', '1');
|
||||||
|
if (dest) window.location.replace(dest);
|
||||||
});
|
});
|
||||||
|
|
||||||
fab.addEventListener('click', async ()=>{ await renderList(); show(); });
|
fab.addEventListener('click', async ()=>{ await renderList(); show(); });
|
||||||
|
|
||||||
|
|
||||||
// Ensure zoom gestures work if the host page tried to disable them
|
// Ensure zoom gestures work if the host page tried to disable them
|
||||||
(function ensureZoomable(){
|
(function ensureZoomable(){
|
||||||
let m = document.querySelector('meta[name=viewport]');
|
let m = document.querySelector('meta[name=viewport]');
|
||||||
@@ -284,4 +362,4 @@
|
|||||||
const c = m.getAttribute('content') || '';
|
const c = m.getAttribute('content') || '';
|
||||||
if (/user-scalable=no|maximum-scale=1/.test(c)) m.setAttribute('content', desired);
|
if (/user-scalable=no|maximum-scale=1/.test(c)) m.setAttribute('content', desired);
|
||||||
})();
|
})();
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user