mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 16:00:25 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fed8a8133 | ||
|
|
a22295ff76 | ||
|
|
402428e86b | ||
|
|
79c4f2c208 | ||
|
|
d392687237 | ||
|
|
389c8617e8 |
No files matched your search
@@ -54,6 +54,13 @@ has no supported injection target in the Frame Control panel.
|
||||
The [browser and fake Frame evidence](evidence/framey-browser-2026-10-25.md)
|
||||
records exactly what was exercised.
|
||||
|
||||
The in-headset **Open official store** view loads the full official catalogue.
|
||||
Search matches names, authors, and descriptions. Filters show non-root plugins
|
||||
or installed plugins, and Previous/Next buttons browse 20 results per page.
|
||||
The root label is a compatibility warning; other APIs are checked only when a
|
||||
plugin is opened. The store still needs the managed Decky user service running
|
||||
before its Install or Update buttons are enabled.
|
||||
|
||||
## For plugin authors
|
||||
|
||||
Publish a normal [Decky store](https://plugins.deckbrew.xyz/) API v2 release
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const html = readFileSync(new URL('../../ui/headset.html', import.meta.url), 'utf8');
|
||||
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1].split('refresh();refreshGames();')[0];
|
||||
class Element {
|
||||
constructor(id='') {this.id=id; this.value=''; this.textContent=''; this.children=[]; this.disabled=false; this.hidden=false;}
|
||||
append(...nodes) {this.children.push(...nodes);}
|
||||
replaceChildren(...nodes) {this.children=nodes; this.textContent='';}
|
||||
scrollIntoView() {}
|
||||
}
|
||||
const elements = new Map();
|
||||
elements.set('deckyStoreFilter',Object.assign(new Element('deckyStoreFilter'),{value:'all'}));
|
||||
const document = {getElementById(id) {if(!elements.has(id)) elements.set(id,new Element(id)); return elements.get(id);},
|
||||
createElement() {return new Element();}, hidden:false};
|
||||
const ctx = vm.createContext({document, location:{hash:'',pathname:'/'},
|
||||
sessionStorage:{getItem:()=>'',setItem(){}}, history:{replaceState(){}},
|
||||
window:{addEventListener(){}}, setInterval(){}, setTimeout(){},
|
||||
fetch(){throw Error('No network expected');}, console});
|
||||
vm.runInContext(script,ctx);
|
||||
const el=id=>document.getElementById(id);
|
||||
const fixtures=Array.from({length:43},(_,i)=>({id:i+1,name:`Plugin ${i+1}`,author:i===42?'Special Author':'Maker',
|
||||
description:'A panel',version:'1.0',root:i===0}));
|
||||
ctx.fixtures=fixtures;
|
||||
vm.runInContext('deckyStore=fixtures; deckyState={running:true,plugins:[{storeId:2,version:"1.0"}]}; showDeckyStore()',ctx);
|
||||
assert.equal(el('deckyStoreSummary').textContent,'43 plugins found');
|
||||
assert.equal(el('deckyStoreList').children.length,20);
|
||||
assert.equal(el('deckyStorePage').textContent,'Page 1 of 3');
|
||||
el('deckyStoreNext').onclick();
|
||||
assert.equal(el('deckyStoreList').children.length,20);
|
||||
el('deckyStoreNext').onclick();
|
||||
assert.equal(el('deckyStoreList').children.length,3);
|
||||
assert.equal(el('deckyStoreNext').disabled,true);
|
||||
el('deckySearch').value='special author'; el('deckySearch').oninput();
|
||||
assert.equal(el('deckyStoreSummary').textContent,'1 plugin found');
|
||||
assert.match(el('deckyStoreList').children[0].children[0].textContent,/Plugin 43/);
|
||||
assert.equal(el('deckyStorePage').textContent,'Page 1 of 1');
|
||||
el('deckySearch').value=''; el('deckyStoreFilter').value='installed'; el('deckyStoreFilter').onchange();
|
||||
assert.equal(el('deckyStoreSummary').textContent,'1 plugin found');
|
||||
assert.match(el('deckyStoreList').children[0].children[0].textContent,/Plugin 2/);
|
||||
el('deckyStoreFilter').value='compatible'; el('deckyStoreFilter').onchange();
|
||||
assert.equal(el('deckyStoreSummary').textContent,'42 plugins found');
|
||||
el('deckySearch').value='no matching plugin'; el('deckySearch').oninput();
|
||||
assert.equal(el('deckyStoreList').textContent,'No plugins match this search.');
|
||||
@@ -30,14 +30,14 @@ def archive(flags=(), extra=None):
|
||||
|
||||
class ArchiveTests(unittest.TestCase):
|
||||
def test_catalog_selects_valid_visible_latest_versions(self):
|
||||
records = [{'id': 42, 'name': 'Example', 'visible': True, 'versions':
|
||||
records = [{'id': 42, 'name': 'Example', 'author': 'Panel Author', 'visible': True, 'versions':
|
||||
[{'name': '1.0', 'hash': 'a' * 64}], 'tags': []},
|
||||
{'id': 43, 'name': 'Hidden', 'visible': False, 'versions':
|
||||
[{'name': '1.0', 'hash': 'b' * 64}]},
|
||||
{'id': 44, 'name': 'Unsafe', 'visible': True, 'versions':
|
||||
[{'name': '1.0; touch /tmp/bad', 'hash': 'c' * 64}]}]
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertEqual([p['id'] for p in frame_decky_store.catalog()], [42])
|
||||
self.assertEqual([(p['id'], p['author']) for p in frame_decky_store.catalog()], [(42, 'Panel Author')])
|
||||
|
||||
def test_accepts_hashed_non_root_archive(self):
|
||||
data, digest = archive()
|
||||
|
||||
@@ -5,6 +5,7 @@ import http.client
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tarfile
|
||||
@@ -19,6 +20,11 @@ import server
|
||||
|
||||
|
||||
class Panel(unittest.TestCase):
|
||||
def test_vr_store_browsing_and_search(self):
|
||||
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_store.mjs')],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
|
||||
@@ -54,8 +54,10 @@ def catalog():
|
||||
if not latest:
|
||||
continue
|
||||
tags = item.get('tags') if isinstance(item.get('tags'), list) else []
|
||||
author = item.get('author') if isinstance(item.get('author'), str) else ''
|
||||
result.append({'id': item['id'], 'name': item['name'][:100],
|
||||
'description': str(item.get('description') or '')[:300],
|
||||
'author': author[:100],
|
||||
'version': latest['name'][:40], 'sha256': latest['hash'],
|
||||
'root': 'root' in tags})
|
||||
return result
|
||||
|
||||
+26
-7
@@ -46,7 +46,10 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
|
||||
<h2>Decky plugins</h2><p>Manage official store plugins here. Decky's Quick Access Menu is absent from this VR shell. Framey can show compatible plugin panels and settings below; Steam library and store page patches are unavailable.</p>
|
||||
<div class="row"><button id="deckyRefresh" class="secondary">Refresh plugins</button><button id="deckyStoreLoad" class="secondary">Open official store</button></div>
|
||||
<p id="deckyState" role="status"></p><div id="deckyInstalled" class="stack"></div>
|
||||
<div id="deckyStorePanel" hidden><label>Find a plugin <input id="deckySearch" type="search" placeholder="Search Decky store"></label><div id="deckyStoreList" class="stack"></div></div>
|
||||
<div id="deckyStorePanel" hidden><div class="row" style="margin:16px 0"><label>Find a plugin <input id="deckySearch" type="search" placeholder="Name, author or description"></label>
|
||||
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">Without root requirement</option><option value="installed">Installed plugins</option></select></label></div>
|
||||
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
|
||||
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
|
||||
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
|
||||
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
|
||||
@@ -97,7 +100,7 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
|
||||
catch(error){byId('actionStatus').textContent=error.message;quit.disabled=false;}};
|
||||
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
|
||||
}catch(error){list.textContent=error.message;}}
|
||||
let deckyState=null, deckyStore=[],activeFrameyId=null;
|
||||
let deckyState=null, deckyStore=[], deckyStoreIndex=0, activeFrameyId=null;
|
||||
async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.':'Decky is not running. Install it from Frame Control on your computer.';
|
||||
const list=byId('deckyInstalled');list.replaceChildren();
|
||||
@@ -109,13 +112,25 @@ async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);}
|
||||
list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.';
|
||||
showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}}
|
||||
async function loadDeckyStore(){try{const data=await api('/api/decky/store');deckyStore=data.plugins;
|
||||
byId('deckyStorePanel').hidden=false;showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}}
|
||||
async function loadDeckyStore(){byId('deckyStorePanel').hidden=false;
|
||||
byId('deckyStoreSummary').textContent='Loading official Decky store…';
|
||||
try{const data=await api('/api/decky/store');deckyStore=data.plugins;deckyStoreIndex=0;showDeckyStore();}
|
||||
catch(error){byId('deckyStoreSummary').textContent=`Store unavailable: ${error.message}`;}}
|
||||
function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren();
|
||||
const q=byId('deckySearch').value.trim().toLowerCase();
|
||||
for(const plugin of deckyStore.filter(p=>!q||`${p.name} ${p.description}`.toLowerCase().includes(q)).slice(0,30)){
|
||||
const filter=byId('deckyStoreFilter').value;
|
||||
const found=deckyStore.filter(p=>(!q||`${p.name} ${p.author||''} ${p.description}`.toLowerCase().includes(q))&&
|
||||
(filter==='all'||(filter==='compatible'&&!p.root)||(filter==='installed'&&(deckyState?.plugins||[]).some(x=>x.storeId===p.id))));
|
||||
const size=20, pages=Math.max(1,Math.ceil(found.length/size));
|
||||
deckyStoreIndex=Math.min(deckyStoreIndex,pages-1);
|
||||
byId('deckyStoreSummary').textContent=`${found.length} plugin${found.length===1?'':'s'} found`;
|
||||
byId('deckyStorePage').textContent=`Page ${deckyStoreIndex+1} of ${pages}`;
|
||||
byId('deckyStorePrevious').disabled=deckyStoreIndex===0;
|
||||
byId('deckyStoreNext').disabled=deckyStoreIndex>=pages-1;
|
||||
if(!found.length)list.textContent='No plugins match this search.';
|
||||
for(const plugin of found.slice(deckyStoreIndex*size,(deckyStoreIndex+1)*size)){
|
||||
const row=document.createElement('div');row.className='block game';
|
||||
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version} · ${plugin.description}`;
|
||||
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version}${plugin.author?' · '+plugin.author:''} · ${plugin.description}`;
|
||||
const button=document.createElement('button');button.textContent='Install';
|
||||
const installed=(deckyState?.plugins||[]).find(p=>p.storeId===plugin.id);
|
||||
if(installed)button.textContent=installed.version===plugin.version?'Reinstall':'Update';
|
||||
@@ -183,7 +198,11 @@ byId('statusScreenClose').onclick=()=>{byId('statusScreen').hidden=true;byId('st
|
||||
byId('statusScreenRefresh').onclick=refresh;
|
||||
window.addEventListener('keydown',event=>{if(event.key==='Escape'&&!byId('statusScreen').hidden)byId('statusScreenClose').click();});
|
||||
byId('gamesRefresh').onclick=refreshGames;byId('record').onclick=toggleRecord;byId('extensionsRefresh').onclick=refreshExtensions;
|
||||
byId('deckyRefresh').onclick=refreshDecky;byId('deckyStoreLoad').onclick=loadDeckyStore;byId('deckySearch').oninput=showDeckyStore;
|
||||
byId('deckyRefresh').onclick=refreshDecky;byId('deckyStoreLoad').onclick=loadDeckyStore;
|
||||
byId('deckySearch').oninput=()=>{deckyStoreIndex=0;showDeckyStore();};
|
||||
byId('deckyStoreFilter').onchange=()=>{deckyStoreIndex=0;showDeckyStore();};
|
||||
byId('deckyStorePrevious').onclick=()=>{deckyStoreIndex--;showDeckyStore();};
|
||||
byId('deckyStoreNext').onclick=()=>{deckyStoreIndex++;showDeckyStore();};
|
||||
byId('extensionClose').onclick=()=>{activeExtension=null;byId('extensionFrame').removeAttribute('src');byId('extensionPanel').hidden=true;};
|
||||
window.addEventListener('message',event=>{if(!activeExtension||event.source!==byId('extensionFrame').contentWindow)return;
|
||||
const request=event.data;if(!request||request.type!=='frame-control:request'||!Number.isSafeInteger(request.id))return;
|
||||
|
||||
Reference in new issue
Block a user