Store: real-source fixes found by running search against live repos

- F-Droid: percent-encode repo file names (a '#' in one screenshot name broke
  the whole main repo); a bad image name drops that image, not the app.
- Search: sources still fetching report 'loading' (UI says so and refreshes
  quietly); indexes warm up at server start; page-only SideQuest is not
  searched and appears as a 'Browse SideQuest' link instead of an error.
- Browse (empty query) ranks VR, artwork and recent updates first; the F-Droid
  archive is off by default (old versions only).
- Throttled sources fall back to their last cached copy; per-host message.
- Curated GitHub list gains Open Saber Plus (MIT) with icon and screenshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 22:02:38 +10:00
1 parent f10b5fe159
commit 69f790b83a
12 files changed
+200 -111

No files matched your search

+11 -5
View File
@@ -1995,18 +1995,24 @@ function renderStore() {
$("searchGrid").innerHTML=`<button class="store-hero" data-app="${apps.indexOf(featured)}" aria-label="Explore ${esc(featured.name)}">${storeArt(o,"",true)}<span class="hero-copy"><span class="store-eyebrow">Step into something new</span><strong>${esc(featured.name)}</strong><span>${esc(storeText(featured.summary))}</span><span class="hero-cta">Explore ${esc(featured.name)} <span aria-hidden="true">&nbsp; ↗</span></span></span></button>` + storeRow(apps.some(a=>a.offers[0].popularity) ? "Popular apps" : "Explore apps","More ways to make your Frame yours",popular) + storeRow(tested.length ? "VR that works on the Frame" : "Step into VR",tested.length ? "Experiences checked on a Frame" : "A new perspective, a whole new playground",vr) + storeRow("Recently updated","Fresh from their creators",recent);
}
function storeChips() { $("searchFilters").querySelectorAll("button").forEach(b=>b.setAttribute("aria-pressed",b.dataset.filter==='all' ? sourceState.vr===null && !sourceState.installable : b.dataset.filter==='installable' ? sourceState.installable : sourceState.vr===(b.dataset.filter==='vr'))); }
async function searchSources() {
async function searchSources(quiet) {
const request=++sourceState.request;
$("searchGrid").setAttribute("aria-busy","true");
$("searchGrid").innerHTML=`<div class="store-grid" role="status" aria-label="Finding apps">${Array.from({length:8},()=>'<div class="store-skeleton" aria-hidden="true"></div>').join("")}</div>`;
if(!quiet)$("searchGrid").innerHTML=`<div class="store-grid" role="status" aria-label="Finding apps">${Array.from({length:8},()=>'<div class="store-skeleton" aria-hidden="true"></div>').join("")}</div>`;
const params=new URLSearchParams({q:$("sourceQ").value,source:sourceState.source,installable:String(sourceState.installable)});
if(sourceState.vr!==null)params.set("vr",String(sourceState.vr));
try {
const result=await api("/api/search?"+params);if(request!==sourceState.request)return;
sourceState.apps=result.apps;
const unavailable=result.sources.filter(s=>s.status==='error'||s.status==='timed out').map(s=>storeName(s.name));
$("sourceStatus").textContent=unavailable.length ? `${unavailable.join(" and ")} ${unavailable.length===1 ? "isn't" : "aren't"} responding right now. You can still explore the other sources.` : "";
$("sourceStatus").hidden=!unavailable.length;
const loading=result.sources.filter(s=>s.status==='loading').map(s=>storeName(s.name));
const notes=[];
if(loading.length)notes.push(`Still fetching ${loading.join(" and ")}; more results will appear in a moment.`);
if(unavailable.length)notes.push(`${unavailable.join(" and ")} ${unavailable.length===1 ? "isn't" : "aren't"} responding right now. You can still explore the other sources.`);
$("sourceStatus").innerHTML=notes.map(esc).join(" ")+(result.elsewhere||[]).map(x=>` <a href="${esc(x.url)}" target="_blank" rel="noopener">Browse ${esc(storeName(x.name))} ↗</a>`).join("");
$("sourceStatus").hidden=!notes.length && !(result.elsewhere||[]).length;
clearTimeout(sourceState.retry);
if(loading.length)sourceState.retry=setTimeout(()=>{ if(request===sourceState.request)searchSources(true); },6000);
renderStore();await loadSources();
} catch(e) { if(request===sourceState.request) { $("searchGrid").innerHTML=storeEmpty();$("sourceStatus").hidden=false;$("sourceStatus").textContent="We couldn't reach the app shelves. Try again in a moment."; } }
finally { if(request===sourceState.request)$("searchGrid").setAttribute("aria-busy","false"); }
@@ -2075,7 +2081,7 @@ $("addSource").onsubmit=e=>{e.preventDefault();const f=new FormData(e.target);ch
document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>$(b.dataset.close).close());
$('appDetail').addEventListener('close',()=>{sourceState.detailRequest++;});
document.querySelectorAll('[data-store-tab]').forEach(b=>b.onclick=()=>{const browse=b.dataset.storeTab==='browse';$('appStore').hidden=!browse;$('androidLibrary').hidden=browse;$('repNew').hidden=browse;$('sourcesOpen').hidden=!browse;document.querySelectorAll('[data-store-tab]').forEach(t=>{t.classList.toggle('on',t===b);t.setAttribute('aria-pressed',t===b);});if(!browse)loadAndroid();});
loadSources().then(searchSources);
loadSources().then(()=>searchSources());
// ---- Android apps (own Lepton instance each) + catalogue ----
const VLABEL = { works: "Works on Frame", likely: "Should work", maybe: "Might work", unlikely: "Probably crashes", no: "Won't work" };