mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 20:59:03 -05:00
289 lines
12 KiB
Plaintext
289 lines
12 KiB
Plaintext
---
|
|
// "Which files do I need": pick a setup and systems, get the file list and
|
|
// the install command. Client side only: reads public/data/picker/*.json,
|
|
// which convert.py derives from install/<platform>.json. The selection
|
|
// follows install.py narrow(): a file is kept when one of its systems is
|
|
// chosen, a file with no system when "other" is chosen.
|
|
// The list follows the current data. release-advertised.json says what the
|
|
// published ZIP of each setup holds, so the page can say when the two
|
|
// differ. generate_site.py writes it from release_record.load_advertised():
|
|
// the release record reduced to the setups the registry marks release_pack,
|
|
// the rule the setup pages and get/ pages follow. A setup the release still
|
|
// holds a pack for but the registry no longer marks gets no ZIP line here.
|
|
import release from '../generated/release-advertised.json';
|
|
|
|
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
|
|
const packs = Object.fromEntries(
|
|
Object.values(release.packs ?? {}).flatMap((pack) =>
|
|
pack.platforms.map((platform) => [platform, { files: pack.files, size: pack.extracted_size }]),
|
|
),
|
|
);
|
|
---
|
|
|
|
<div class="rb-picker" data-base={base} data-release={JSON.stringify({ tag: release.tag ?? '', packs })}>
|
|
<fieldset>
|
|
<label for="rb-pick-platform">Your setup</label>
|
|
<select id="rb-pick-platform" name="platform">
|
|
<option value="">Choose a setup</option>
|
|
</select>
|
|
</fieldset>
|
|
<div class="rb-pick-systems" hidden>
|
|
<p class="rb-pick-systems-head">
|
|
<strong>Systems</strong>
|
|
<button type="button" class="rb-text-button" data-all>All</button>
|
|
<button type="button" class="rb-text-button" data-none>None</button>
|
|
</p>
|
|
<input type="search" class="rb-pick-filter" placeholder="Type a console" aria-label="Type a console" />
|
|
<div class="rb-pick-list" role="group" aria-label="Systems"></div>
|
|
</div>
|
|
<div class="rb-pick-result" role="status" aria-live="polite"></div>
|
|
<p class="rb-pick-release" hidden></p>
|
|
<div class="rb-pick-output" hidden>
|
|
<p><strong>Install command</strong> for Linux, macOS, Steam Deck and handhelds</p>
|
|
<figure class="rb-pick-frame">
|
|
<figcaption>Terminal</figcaption>
|
|
<pre class="rb-pick-cmd"><code></code></pre>
|
|
</figure>
|
|
<button type="button" data-copy>Copy the command</button>
|
|
<details>
|
|
<summary>Files and where they go</summary>
|
|
<table>
|
|
<thead><tr><th scope="col">Destination</th><th scope="col">Size</th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
type Data = {
|
|
platform: string;
|
|
name: string;
|
|
folder: string;
|
|
generated: string;
|
|
systems: { id: string; title: string; native: string[] }[];
|
|
other: boolean;
|
|
files: [string, number, number[]][];
|
|
};
|
|
|
|
type Release = { tag: string; packs: Record<string, { files: number; size: number }> };
|
|
|
|
const INSTALL = 'curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s --';
|
|
const MAX_ROWS = 500;
|
|
// A word that holds a hyphen (--platform, nintendo-64dd) never breaks
|
|
// inside: the same rule as the command blocks (ec-unbroken-tokens.mjs).
|
|
// Only spans are added, the text copied is the command itself.
|
|
const UNBROKEN = /(?<!\S)(?=\S*-)\S{1,48}(?!\S)/g;
|
|
|
|
const showCommand = (target: HTMLElement, command: string) => {
|
|
const nodes: (Node | string)[] = [];
|
|
let last = 0;
|
|
for (const match of command.matchAll(UNBROKEN)) {
|
|
nodes.push(command.slice(last, match.index));
|
|
const word = document.createElement('span');
|
|
word.className = 'rb-nb';
|
|
word.textContent = match[0];
|
|
nodes.push(word);
|
|
last = match.index! + match[0].length;
|
|
}
|
|
nodes.push(command.slice(last));
|
|
target.replaceChildren(...nodes);
|
|
};
|
|
|
|
const size = (bytes: number) =>
|
|
bytes >= 1024 ** 3 ? `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
|
: bytes >= 1024 ** 2 ? `${(bytes / 1024 ** 2).toFixed(1)} MB`
|
|
: bytes >= 1024 ? `${(bytes / 1024).toFixed(1)} KB`
|
|
: `${bytes} B`;
|
|
|
|
for (const root of document.querySelectorAll<HTMLElement>('.rb-picker')) {
|
|
const base = root.dataset.base ?? '/';
|
|
const select = root.querySelector('select')!;
|
|
const systemsBox = root.querySelector<HTMLElement>('.rb-pick-systems')!;
|
|
const list = root.querySelector<HTMLElement>('.rb-pick-list')!;
|
|
const filter = root.querySelector<HTMLInputElement>('.rb-pick-filter')!;
|
|
const result = root.querySelector<HTMLElement>('.rb-pick-result')!;
|
|
const output = root.querySelector<HTMLElement>('.rb-pick-output')!;
|
|
const cmd = root.querySelector<HTMLElement>('.rb-pick-cmd code')!;
|
|
const tbody = root.querySelector('tbody')!;
|
|
const releaseLine = root.querySelector<HTMLElement>('.rb-pick-release')!;
|
|
const release: Release = JSON.parse(root.dataset.release ?? '{"tag":"","packs":{}}');
|
|
const count = (n: number) => n.toLocaleString('en');
|
|
let data: Data | null = null;
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
const index: { platform: string; name: string }[] = await fetch(`${base}data/picker/index.json`).then((r) => r.json());
|
|
for (const entry of index) select.add(new Option(entry.name, entry.platform));
|
|
|
|
const chosen = () => [...list.querySelectorAll<HTMLInputElement>('input:checked')].map((box) => box.value);
|
|
|
|
const render = () => {
|
|
if (!data) return;
|
|
const picked = chosen();
|
|
const wanted = new Set(picked.map((id) => data!.systems.findIndex((s) => s.id === id)));
|
|
const other = picked.includes('other');
|
|
const files = data.files.filter(([, , systems]) =>
|
|
systems.length ? systems.some((s) => wanted.has(s)) : other);
|
|
const total = files.reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
const all = picked.length === data.systems.length + (data.other ? 1 : 0);
|
|
result.textContent = picked.length
|
|
? `${count(files.length)} files, ${size(total)}, for ${count(picked.length)} of ${count(data.systems.length + (data.other ? 1 : 0))} choices. Folder: ${data.folder || 'the setup BIOS folder'}.`
|
|
: 'Choose at least one system.';
|
|
output.hidden = !picked.length;
|
|
showCommand(cmd, `${INSTALL} --platform ${data.platform}${all ? '' : ` --system ${picked.join(',')}`}`);
|
|
tbody.replaceChildren(...files.slice(0, MAX_ROWS).map(([dest, bytes]) => {
|
|
const row = document.createElement('tr');
|
|
const where = document.createElement('td');
|
|
const code = document.createElement('code');
|
|
code.textContent = dest;
|
|
where.append(code);
|
|
const weight = document.createElement('td');
|
|
weight.textContent = size(bytes);
|
|
row.append(where, weight);
|
|
return row;
|
|
}));
|
|
if (files.length > MAX_ROWS) {
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 2;
|
|
cell.textContent = `and ${count(files.length - MAX_ROWS)} more.`;
|
|
row.append(cell);
|
|
tbody.append(row);
|
|
}
|
|
const url = new URL(location.href);
|
|
url.searchParams.set('platform', data.platform);
|
|
if (all) url.searchParams.delete('system');
|
|
else url.searchParams.set('system', picked.join(','));
|
|
history.replaceState(null, '', url);
|
|
};
|
|
|
|
const load = async (platform: string, preset: string[] | null) => {
|
|
list.replaceChildren();
|
|
output.hidden = true;
|
|
releaseLine.hidden = true;
|
|
systemsBox.hidden = !platform;
|
|
if (!platform) { result.textContent = ''; return; }
|
|
result.textContent = 'Loading the file list...';
|
|
data = await fetch(`${base}data/picker/${platform}.json`).then((r) => r.json());
|
|
// Sorted by the name a reader looks for, not by id.
|
|
const choices = data!.systems
|
|
.map((s) => ({
|
|
id: s.id,
|
|
title: s.title || s.id,
|
|
detail: [s.id, ...s.native].filter((n) => n !== s.title).join(', '),
|
|
}))
|
|
.sort((a, b) => a.title.localeCompare(b.title, 'en'));
|
|
if (data!.other) choices.push({ id: 'other', title: 'Other files', detail: 'engines and computers no system owns' });
|
|
for (const choice of choices) {
|
|
const label = document.createElement('label');
|
|
const box = document.createElement('input');
|
|
box.type = 'checkbox';
|
|
box.value = choice.id;
|
|
box.checked = preset ? preset.includes(choice.id) : false;
|
|
const detail = document.createElement('small');
|
|
detail.textContent = choice.detail;
|
|
label.append(box, ` ${choice.title} `, detail);
|
|
list.append(label);
|
|
}
|
|
const pack = release.packs[data!.platform];
|
|
const listed = data!.files.reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
if (pack && (pack.files !== data!.files.length || pack.size !== listed)) {
|
|
releaseLine.textContent = `The ZIP of release ${release.tag} holds ${count(pack.files)} files, ${size(pack.size)}. It was built from the list of that day. The command installs the current list.`;
|
|
releaseLine.hidden = false;
|
|
}
|
|
applyFilter();
|
|
render();
|
|
};
|
|
|
|
select.addEventListener('change', async () => {
|
|
await load(select.value, null);
|
|
if (select.value) filter.focus();
|
|
});
|
|
list.addEventListener('change', render);
|
|
const applyFilter = () => {
|
|
const query = filter.value.trim().toLowerCase();
|
|
for (const label of list.querySelectorAll<HTMLElement>('label')) {
|
|
label.hidden = !!query && !label.textContent!.toLowerCase().includes(query);
|
|
}
|
|
};
|
|
filter.addEventListener('input', applyFilter);
|
|
root.querySelector('[data-all]')!.addEventListener('click', () => {
|
|
list.querySelectorAll<HTMLInputElement>('label:not([hidden]) input').forEach((box) => (box.checked = true));
|
|
render();
|
|
});
|
|
root.querySelector('[data-none]')!.addEventListener('click', () => {
|
|
list.querySelectorAll<HTMLInputElement>('label:not([hidden]) input').forEach((box) => (box.checked = false));
|
|
render();
|
|
});
|
|
root.querySelector('[data-copy]')!.addEventListener('click', () => navigator.clipboard?.writeText(cmd.textContent ?? ''));
|
|
|
|
const initial = params.get('platform');
|
|
if (initial && index.some((entry) => entry.platform === initial)) {
|
|
select.value = initial;
|
|
await load(initial, params.get('system')?.split(',') ?? null);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Global: the checkboxes are created by the script, outside Astro's scoped styles. -->
|
|
<style is:global>
|
|
.rb-picker .rb-pick-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
|
gap: 0.25rem 1rem;
|
|
max-height: 22rem;
|
|
overflow: auto;
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--rb-border);
|
|
border-radius: 4px;
|
|
}
|
|
.rb-picker .rb-pick-list label[hidden] { display: none; }
|
|
.rb-picker .rb-pick-list small { color: var(--sl-color-gray-3); }
|
|
.rb-picker .rb-pick-list label { display: flex; gap: 0.5rem; align-items: center; min-height: 32px; margin: 0; font-size: 0.9rem; }
|
|
.sl-markdown-content .rb-picker .rb-pick-list input { min-height: auto; width: 1.1rem; height: 1.1rem; margin: 0; flex: none; }
|
|
.rb-picker .rb-pick-systems-head { display: flex; gap: 0.5rem; align-items: center; }
|
|
.rb-picker .rb-pick-systems-head > * { margin: 0; }
|
|
.rb-picker .rb-pick-filter { width: 100%; margin: 0.5rem 0; }
|
|
.rb-picker .rb-pick-cmd code { white-space: pre-wrap; overflow-wrap: break-word; }
|
|
.rb-picker .rb-pick-cmd .rb-nb { white-space: nowrap; }
|
|
.rb-picker .rb-pick-result { margin-top: 1rem; font-weight: 600; }
|
|
.rb-picker fieldset {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.35rem;
|
|
border: 0;
|
|
padding: 0;
|
|
}
|
|
.rb-picker fieldset label { font-weight: 600; }
|
|
.rb-picker .rb-pick-release { margin-top: 0.5rem; color: var(--sl-color-gray-2); }
|
|
/* All and None act on the list: plain text, not two primary buttons. */
|
|
.sl-markdown-content .rb-picker .rb-pick-systems-head .rb-text-button {
|
|
min-height: 32px;
|
|
padding: 0 0.25rem;
|
|
background: none;
|
|
color: var(--rb-link);
|
|
font-weight: 400;
|
|
text-decoration: underline;
|
|
}
|
|
/* The command in the same titled frame as the page's other commands. */
|
|
.rb-picker .rb-pick-frame {
|
|
margin: 0.5rem 0;
|
|
border: 1px solid var(--sl-color-gray-5);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.rb-picker .rb-pick-frame figcaption {
|
|
padding: 0.35rem 1rem;
|
|
border-bottom: 1px solid var(--sl-color-gray-5);
|
|
background: var(--sl-color-gray-6);
|
|
color: var(--sl-color-gray-2);
|
|
font-size: var(--sl-text-xs);
|
|
}
|
|
.rb-picker .rb-pick-frame pre {
|
|
margin: 0;
|
|
padding: 0.75rem 1rem;
|
|
border: 0;
|
|
border-radius: 0;
|
|
}
|
|
</style>
|