One /stores page for both stores, with a device chooser
The Batocera store had /batocera to itself. A RetroArch store now serves the same cartridges on every other machine, and copying the page would have duplicated everything the two have in common — the framing, the platform table, the engine links — so they share one page and a Batocera/RetroArch chooser. Only the install command, the CLI and the uninstall line change with the tab. /batocera and /retroarch both redirect here with the matching tab preselected, so old links keep working and the name someone guesses after reading "RetroArch store" lands somewhere useful. `?device=` makes a link to either half shareable. The page also documents uninstalling, which it never did, and the five command blocks share one CommandBlock component instead of repeating the copy button. The i18n `batocera` block becomes `stores` in both locales; every key the page uses was checked to exist in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -198,23 +198,35 @@ export default {
|
||||
title: 'Build Matrix',
|
||||
subtitle: 'Which engines build for which platforms.',
|
||||
},
|
||||
batocera: {
|
||||
title: 'Batocera Store',
|
||||
subtitle: 'Our catalog, straight on your retro box.',
|
||||
lead: 'ttg-batocera-store puts our catalog on Batocera, the plug-and-play retro gaming distribution. It pulls our games into the box\'s ROM folders together with box art and EmulationStation metadata, and can be re-run any time from the Ports menu to pick up new releases. Python 3 standard library only — nothing to install alongside it.',
|
||||
stores: {
|
||||
title: 'Stores',
|
||||
subtitle: 'Our games on your own machine, kept up to date.',
|
||||
lead: 'Two stores put our catalog on the two things that play it: a Batocera box, and RetroArch on anything else — desktop Linux, Windows, macOS, Android, a Steam Deck or a handheld. Both download the games straight from this site with box art, add them to the device\'s own library, and can be re-run any time to pick up new releases. Python 3 standard library only — nothing to install alongside them.',
|
||||
chooseTitle: 'Pick your device',
|
||||
repo: 'Git repository',
|
||||
docs: 'Documentation',
|
||||
batoceraProject: 'Batocera project',
|
||||
installTitle: 'Install',
|
||||
installDesc: 'Run this over SSH on the Batocera box. It installs the client, writes the config, creates the Ports entry and runs the first sync.',
|
||||
installNote: 'Then restart EmulationStation so the games show up:',
|
||||
useTitle: 'Use it',
|
||||
useDesc: 'On the device: Ports ▸ "Teletype Games Store". Over SSH the client has a small CLI:',
|
||||
platformsTitle: 'What it installs',
|
||||
platformsDesc: 'A catalog platform is installable when its release asset boots directly on a Batocera system. Other platforms ship web and desktop builds instead, so they are not mapped.',
|
||||
uninstallTitle: 'Remove it',
|
||||
uninstallDesc: 'One line takes it all back out: the games, the metadata, the store itself.',
|
||||
uninstallNote: 'Your own games are never touched. Anything you put in the store\'s folder keeps its place, your play counts and favourites survive, and the backups the store made of files it found are listed rather than deleted.',
|
||||
platformsTitle: 'What they install',
|
||||
platformsDesc: 'A catalog platform is installable when its release file boots directly in an emulator — the same cartridges either way. Our other platforms ship web and desktop builds, which no emulator loads; on a Batocera box those can be installed as Ports instead.',
|
||||
engineTitle: 'Run your own store',
|
||||
engineDesc: 'Nothing here is specific to us. Our store is just a config file on top of warp-engine-batocera-store, an open engine that works with any WarpEngine-based site: point it at your own catalog, give it a name and a ROM subfolder of its own, and your games land on a Batocera box the same way. Several stores can live side by side on one machine without touching each other\'s games.',
|
||||
engineDesc: 'Nothing here is specific to us. Each store is just a config file on top of an open engine that works with any WarpEngine-based site: point it at your own catalog, give it a name and a folder of its own, and your games land on a device the same way. The two engines share warpstore, which does the catalog side for both, so a third kind of device is an adapter and a config rather than a new program. Several stores can live side by side on one machine without touching each other\'s games.',
|
||||
copy: 'Copy',
|
||||
batocera: {
|
||||
tagline: 'The plug-and-play retro distribution',
|
||||
installDesc: 'Run this over SSH on the Batocera box. It installs the engine, writes the config, creates the Ports entry and runs the first sync.',
|
||||
installNote: 'Then restart EmulationStation so the games show up:',
|
||||
useDesc: 'On the device: Ports ▸ "Teletype Games Store". Over SSH the store has a small CLI:',
|
||||
},
|
||||
retroarch: {
|
||||
tagline: 'Desktop, Android, Steam Deck, handhelds',
|
||||
installDesc: 'Run this on the machine RetroArch is installed on. It reads your retroarch.cfg to find where your playlists, thumbnails and cores live, then writes one playlist per platform with box art.',
|
||||
installNote: 'Restart RetroArch afterwards — it builds its menu at startup. If the VICE x64 and TIC-80 cores are missing it still works, and RetroArch asks which core to use the first time.',
|
||||
useDesc: 'The store has a small CLI. Close RetroArch before syncing: it writes its playlists back when it exits, so a sync underneath a running instance can be undone.',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: 'Codebase',
|
||||
|
||||
@@ -198,23 +198,35 @@ export default {
|
||||
title: 'Build mátrix',
|
||||
subtitle: 'Melyik engine melyik platformra fordít.',
|
||||
},
|
||||
batocera: {
|
||||
title: 'Batocera Store',
|
||||
subtitle: 'A katalógusunk közvetlenül a retró gépeden.',
|
||||
lead: 'A ttg-batocera-store a katalógusunkat teszi fel a Batocerára, a plug-and-play retró gamer disztribúcióra. A játékainkat a gép ROM mappáiba tölti le, borítóképpel és EmulationStation metaadatokkal együtt, és a Ports menüből bármikor újrafuttatható az új kiadásokért. Csak a Python 3 alapkönyvtárát használja — nem kell mellé semmit telepíteni.',
|
||||
stores: {
|
||||
title: 'Store-ok',
|
||||
subtitle: 'A játékaink a saját gépeden, mindig frissen.',
|
||||
lead: 'Két store teszi fel a katalógusunkat arra a kettőre, ami le is játssza: egy Batocera gépre, és RetroArch-ra minden máson — asztali Linuxon, Windowson, macOS-en, Androidon, Steam Decken vagy egy kézikonzolon. Mindkettő közvetlenül erről az oldalról tölti le a játékokat borítóképpel, felveszi őket a gép saját könyvtárába, és bármikor újrafuttatható az új kiadásokért. Csak a Python 3 alapkönyvtárát használják — nem kell mellé semmit telepíteni.',
|
||||
chooseTitle: 'Válaszd ki a gépedet',
|
||||
repo: 'Git tároló',
|
||||
docs: 'Dokumentáció',
|
||||
batoceraProject: 'Batocera projekt',
|
||||
installTitle: 'Telepítés',
|
||||
installDesc: 'Futtasd ezt SSH-n a Batocera gépen. Telepíti a klienst, kiírja a konfigurációt, létrehozza a Ports bejegyzést és lefuttatja az első szinkront.',
|
||||
installNote: 'Utána indítsd újra az EmulationStationt, hogy megjelenjenek a játékok:',
|
||||
useTitle: 'Használat',
|
||||
useDesc: 'A gépen: Ports ▸ „Teletype Games Store”. SSH-n keresztül egy egyszerű CLI áll rendelkezésre:',
|
||||
platformsTitle: 'Mit telepít',
|
||||
platformsDesc: 'Egy katalógus-platform akkor telepíthető, ha a kiadás fájlja közvetlenül elindul egy Batocera rendszeren. A többi platform webes és asztali buildeket ad, ezért nincsenek leképezve.',
|
||||
uninstallTitle: 'Eltávolítás',
|
||||
uninstallDesc: 'Egy sor mindent visszabont: a játékokat, a metaadatokat és magát a store-t.',
|
||||
uninstallNote: 'A saját játékaidhoz nem nyúl. Ami a store mappájába tőled került, a helyén marad, a játékszámláló és a kedvencek megmaradnak, a store által készített biztonsági másolatokat pedig felsorolja, nem törli.',
|
||||
platformsTitle: 'Mit telepítenek',
|
||||
platformsDesc: 'Egy katalógus-platform akkor telepíthető, ha a kiadás fájlja közvetlenül elindul egy emulátorban — mindkét úton ugyanezek. A többi platformunk webes és asztali buildet ad, amit emulátor nem tölt be; Batocera gépen azok Ports-ként telepíthetők.',
|
||||
engineTitle: 'Csinálj saját store-t',
|
||||
engineDesc: 'Itt semmi sem ránk van szabva. A mi store-unk csak egy konfigurációs fájl a warp-engine-batocera-store fölött, ami bármilyen WarpEngine alapú oldallal működik: állítsd a saját katalógusodra, adj neki nevet és saját ROM almappát, és a játékaid ugyanígy kerülnek fel egy Batocera gépre. Egy gépen több store is megfér egymás mellett anélkül, hogy egymás játékaihoz nyúlnának.',
|
||||
engineDesc: 'Itt semmi sem ránk van szabva. Mindkét store csak egy konfigurációs fájl egy nyílt motor fölött, ami bármilyen WarpEngine alapú oldallal működik: állítsd a saját katalógusodra, adj neki nevet és saját mappát, és a játékaid ugyanígy kerülnek fel egy gépre. A két motor a warpstore-on osztozik, ami mindkettőnek a katalógus-oldalt viszi, tehát egy harmadik fajta gép már csak egy adapter és egy konfiguráció, nem új program. Egy gépen több store is megfér egymás mellett anélkül, hogy egymás játékaihoz nyúlnának.',
|
||||
copy: 'Másolás',
|
||||
batocera: {
|
||||
tagline: 'A plug-and-play retró disztribúció',
|
||||
installDesc: 'Futtasd ezt SSH-n a Batocera gépen. Telepíti a motort, kiírja a konfigurációt, létrehozza a Ports bejegyzést és lefuttatja az első szinkront.',
|
||||
installNote: 'Utána indítsd újra az EmulationStationt, hogy megjelenjenek a játékok:',
|
||||
useDesc: 'A gépen: Ports ▸ „Teletype Games Store”. SSH-n keresztül egy egyszerű CLI áll rendelkezésre:',
|
||||
},
|
||||
retroarch: {
|
||||
tagline: 'Asztali gép, Android, Steam Deck, kézikonzol',
|
||||
installDesc: 'Futtasd azon a gépen, amin a RetroArch van. A retroarch.cfg-ből olvassa ki, hol vannak a playlistjeid, a borítóképek és a magok, majd platformonként egy playlistet ír borítóképpel.',
|
||||
installNote: 'Utána indítsd újra a RetroArch-ot — a menüt indulásnál építi. Ha a VICE x64 és a TIC-80 mag nincs telepítve, akkor is működik: a RetroArch az első indításnál rákérdez, melyik maggal futtassa.',
|
||||
useDesc: 'A store-nak van egy egyszerű CLI-je. Szinkron előtt zárd be a RetroArch-ot: kilépéskor visszaírja a playlistjeit, tehát a futó példány alatti szinkron elveszhet.',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: 'Kódbázis',
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-emerald-700 to-teal-800 py-16">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('batocera.title') }}</h1>
|
||||
<p class="hero-subtitle text-emerald-50">{{ t('batocera.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-12 px-4 mt-0">
|
||||
<div class="bat-panel">
|
||||
<p class="bat-lead">{{ t('batocera.lead') }}</p>
|
||||
|
||||
<div class="bat-links">
|
||||
<a :href="REPO_URL" target="_blank" rel="noopener noreferrer" class="bat-link bat-link-dark">
|
||||
<i class="fa-solid fa-code-branch"></i> {{ t('batocera.repo') }}
|
||||
</a>
|
||||
<a :href="WIKI_URL" target="_blank" rel="noopener noreferrer" class="bat-link bat-link-indigo">
|
||||
<i class="fa-solid fa-book"></i> {{ t('batocera.docs') }}
|
||||
</a>
|
||||
<a :href="BATOCERA_URL" target="_blank" rel="noopener noreferrer" class="bat-link bat-link-emerald">
|
||||
<i class="fa-solid fa-tv"></i> {{ t('batocera.batoceraProject') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="bat-section">
|
||||
<h2 class="bat-section-title">
|
||||
<span class="bat-step">1</span> {{ t('batocera.installTitle') }}
|
||||
</h2>
|
||||
<p class="bat-section-desc">{{ t('batocera.installDesc') }}</p>
|
||||
<div class="bat-code">
|
||||
<pre><code>{{ INSTALL_CMD }}</code></pre>
|
||||
<button class="bat-copy" :title="t('batocera.copy')" @click="copy(INSTALL_CMD)">
|
||||
<i :class="copied === INSTALL_CMD ? 'fa-solid fa-check' : 'fa-regular fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="bat-note">{{ t('batocera.installNote') }}</p>
|
||||
<div class="bat-code">
|
||||
<pre><code>{{ RESTART_CMD }}</code></pre>
|
||||
<button class="bat-copy" :title="t('batocera.copy')" @click="copy(RESTART_CMD)">
|
||||
<i :class="copied === RESTART_CMD ? 'fa-solid fa-check' : 'fa-regular fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bat-section">
|
||||
<h2 class="bat-section-title">
|
||||
<span class="bat-step">2</span> {{ t('batocera.useTitle') }}
|
||||
</h2>
|
||||
<p class="bat-section-desc">{{ t('batocera.useDesc') }}</p>
|
||||
<div class="bat-code">
|
||||
<pre><code>{{ CLI_SNIPPET }}</code></pre>
|
||||
<button class="bat-copy" :title="t('batocera.copy')" @click="copy(CLI_SNIPPET)">
|
||||
<i :class="copied === CLI_SNIPPET ? 'fa-solid fa-check' : 'fa-regular fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bat-section">
|
||||
<h2 class="bat-section-title">{{ t('batocera.platformsTitle') }}</h2>
|
||||
<p class="bat-section-desc">{{ t('batocera.platformsDesc') }}</p>
|
||||
<ul class="bat-platforms">
|
||||
<li v-for="p in platforms" :key="p.platform" class="bat-platform">
|
||||
<i :class="p.icon" class="bat-platform-icon"></i>
|
||||
<span class="bat-platform-name">{{ p.label }}</span>
|
||||
<code class="bat-platform-ext">{{ p.ext }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="bat-section bat-engine">
|
||||
<h2 class="bat-section-title">
|
||||
<i class="fa-solid fa-cubes bat-engine-icon"></i> {{ t('batocera.engineTitle') }}
|
||||
</h2>
|
||||
<p class="bat-section-desc">{{ t('batocera.engineDesc') }}</p>
|
||||
<a :href="ENGINE_URL" target="_blank" rel="noopener noreferrer" class="bat-link bat-link-slate">
|
||||
<i class="fa-solid fa-code-branch"></i> warp-engine-batocera-store
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<div class="bat-back">
|
||||
<RouterLink to="/catalog" class="bat-back-link">{{ t('catalogShow.back') }}</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CONFIG } from '../../lib/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const FORGE = 'https://git.teletypegames.org/tools'
|
||||
const REPO_URL = `${FORGE}/ttg-batocera-store`
|
||||
// The store is only a config; the client itself is a reusable engine that runs
|
||||
// against any WarpEngine site, so it gets its own repository and its own link.
|
||||
const ENGINE_URL = `${FORGE}/warp-engine-batocera-store`
|
||||
const WIKI_URL = `${CONFIG.wikiBase}/others/ttg-batocera-store`
|
||||
const BATOCERA_URL = 'https://batocera.org'
|
||||
|
||||
// The installer is served straight from the forge, so this one line is the
|
||||
// whole install on a Batocera box.
|
||||
const INSTALL_CMD = `curl -fsSL ${REPO_URL}/raw/branch/master/install.sh | bash`
|
||||
const RESTART_CMD = 'batocera-es-swissknife --restart'
|
||||
const STORE_CLI = '/userdata/system/batocera-store/ttg-store'
|
||||
const CLI_SNIPPET = [
|
||||
`${STORE_CLI} list # compatible catalog entries`,
|
||||
`${STORE_CLI} sync # download everything new`,
|
||||
`${STORE_CLI} remove c64demo`,
|
||||
].join('\n')
|
||||
|
||||
const platforms = [
|
||||
{ platform: 'c64', label: 'Commodore 64 (VICE)', ext: '.prg', icon: 'fa-solid fa-floppy-disk' },
|
||||
{ platform: 'tic80', label: 'TIC-80', ext: '.tic', icon: 'fa-solid fa-tv' },
|
||||
]
|
||||
|
||||
const copied = ref('')
|
||||
|
||||
async function copy(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = text
|
||||
setTimeout(() => { if (copied.value === text) copied.value = '' }, 2000)
|
||||
} catch {
|
||||
// Clipboard API needs a secure context — the command stays selectable anyway.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bat-panel {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 p-6 md:p-8;
|
||||
}
|
||||
.bat-lead {
|
||||
@apply text-gray-600 leading-relaxed mb-6;
|
||||
}
|
||||
.bat-links {
|
||||
@apply flex flex-wrap gap-3;
|
||||
}
|
||||
.bat-link {
|
||||
@apply inline-flex items-center gap-2 font-bold py-2.5 px-5 rounded-xl text-sm text-white transition-all active:scale-95;
|
||||
}
|
||||
.bat-link-dark { @apply bg-slate-800 hover:bg-slate-900 shadow-lg shadow-slate-800/20; }
|
||||
.bat-link-slate { @apply bg-slate-600 hover:bg-slate-700 shadow-lg shadow-slate-600/20; }
|
||||
.bat-link-indigo { @apply bg-indigo-600 hover:bg-indigo-700 shadow-lg shadow-indigo-600/20; }
|
||||
.bat-link-emerald { @apply bg-emerald-600 hover:bg-emerald-700 shadow-lg shadow-emerald-600/20; }
|
||||
|
||||
.bat-section {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 p-6 md:p-8 mt-6;
|
||||
}
|
||||
.bat-section-title {
|
||||
@apply flex items-center gap-3 text-xl font-bold text-gray-900 mb-2;
|
||||
}
|
||||
.bat-step {
|
||||
@apply inline-flex items-center justify-center w-7 h-7 rounded-full bg-emerald-100 text-emerald-700 text-sm font-bold;
|
||||
}
|
||||
.bat-section-desc {
|
||||
@apply text-gray-600 mb-4;
|
||||
}
|
||||
.bat-note {
|
||||
@apply text-gray-600 mt-4 mb-3 text-sm;
|
||||
}
|
||||
.bat-code {
|
||||
@apply relative bg-gray-900 text-gray-100 rounded-xl overflow-x-auto;
|
||||
}
|
||||
.bat-code pre {
|
||||
@apply p-4 pr-14 font-mono text-sm leading-relaxed;
|
||||
}
|
||||
.bat-copy {
|
||||
@apply absolute top-2 right-2 w-9 h-9 rounded-lg bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors;
|
||||
}
|
||||
.bat-engine {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
.bat-engine-icon {
|
||||
@apply text-gray-400 text-lg;
|
||||
}
|
||||
.bat-platforms {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 gap-3;
|
||||
}
|
||||
.bat-platform {
|
||||
@apply flex items-center gap-3 bg-gray-50 border border-gray-100 rounded-xl px-4 py-3;
|
||||
}
|
||||
.bat-platform-icon {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.bat-platform-name {
|
||||
@apply font-medium text-gray-800 flex-grow;
|
||||
}
|
||||
.bat-platform-ext {
|
||||
@apply font-mono text-xs text-purple-600 bg-purple-50 px-2 py-0.5 rounded;
|
||||
}
|
||||
.bat-back {
|
||||
@apply mt-8 text-center;
|
||||
}
|
||||
.bat-back-link {
|
||||
@apply text-gray-400 hover:text-gray-600 text-sm font-medium transition-colors;
|
||||
}
|
||||
</style>
|
||||
@@ -67,8 +67,8 @@
|
||||
<i class="fa-solid fa-table-cells"></i> {{ t('builds.title') }}
|
||||
</RouterLink>
|
||||
<span class="builds-link-sep">|</span>
|
||||
<RouterLink to="/batocera" class="builds-link">
|
||||
<i class="fa-solid fa-gamepad"></i> {{ t('batocera.title') }}
|
||||
<RouterLink to="/stores" class="builds-link">
|
||||
<i class="fa-solid fa-gamepad"></i> {{ t('stores.title') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="cb">
|
||||
<pre><code>{{ command }}</code></pre>
|
||||
<button class="cb-copy" :title="t('stores.copy')" @click="copy">
|
||||
<i :class="copied ? 'fa-solid fa-check' : 'fa-regular fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// The page shows five of these — install, restart, CLI, uninstall — so the
|
||||
// block and its copy button live in one place rather than being repeated.
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { command } = defineProps<{ command: string }>()
|
||||
const { t } = useI18n()
|
||||
const copied = ref(false)
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
} catch {
|
||||
// Clipboard API needs a secure context — the command stays selectable anyway.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cb {
|
||||
@apply relative bg-gray-900 text-gray-100 rounded-xl overflow-x-auto;
|
||||
}
|
||||
.cb pre {
|
||||
@apply p-4 pr-14 font-mono text-sm leading-relaxed;
|
||||
}
|
||||
.cb-copy {
|
||||
@apply absolute top-2 right-2 w-9 h-9 rounded-lg bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-emerald-700 to-teal-800 py-16">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('stores.title') }}</h1>
|
||||
<p class="hero-subtitle text-emerald-50">{{ t('stores.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-12 px-4 mt-0">
|
||||
<div class="st-panel">
|
||||
<p class="st-lead">{{ t('stores.lead') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Both stores install the same cartridges; what differs is the host, so
|
||||
the device is the only thing the visitor has to choose. -->
|
||||
<section class="st-section">
|
||||
<h2 class="st-section-title">{{ t('stores.chooseTitle') }}</h2>
|
||||
<div class="st-devices" role="tablist">
|
||||
<button
|
||||
v-for="d in devices"
|
||||
:key="d.id"
|
||||
class="st-device"
|
||||
:class="{ 'st-device-active': d.id === device }"
|
||||
role="tab"
|
||||
:aria-selected="d.id === device"
|
||||
@click="select(d.id)"
|
||||
>
|
||||
<i :class="d.icon" class="st-device-icon"></i>
|
||||
<span class="st-device-name">{{ d.name }}</span>
|
||||
<span class="st-device-tagline">{{ t(`stores.${d.id}.tagline`) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="st-links">
|
||||
<a :href="current.repoUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-dark">
|
||||
<i class="fa-solid fa-code-branch"></i> {{ t('stores.repo') }}
|
||||
</a>
|
||||
<a :href="current.wikiUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-indigo">
|
||||
<i class="fa-solid fa-book"></i> {{ t('stores.docs') }}
|
||||
</a>
|
||||
<a :href="current.projectUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-emerald">
|
||||
<i :class="current.icon"></i> {{ current.name }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="st-section">
|
||||
<h2 class="st-section-title">
|
||||
<span class="st-step">1</span> {{ t('stores.installTitle') }}
|
||||
</h2>
|
||||
<p class="st-section-desc">{{ t(`stores.${device}.installDesc`) }}</p>
|
||||
<CommandBlock :command="current.installCmd" />
|
||||
<template v-if="current.afterInstallCmd">
|
||||
<p class="st-note">{{ t(`stores.${device}.installNote`) }}</p>
|
||||
<CommandBlock :command="current.afterInstallCmd" />
|
||||
</template>
|
||||
<p v-else class="st-note">{{ t(`stores.${device}.installNote`) }}</p>
|
||||
</section>
|
||||
|
||||
<section class="st-section">
|
||||
<h2 class="st-section-title">
|
||||
<span class="st-step">2</span> {{ t('stores.useTitle') }}
|
||||
</h2>
|
||||
<p class="st-section-desc">{{ t(`stores.${device}.useDesc`) }}</p>
|
||||
<CommandBlock :command="current.cliSnippet" />
|
||||
</section>
|
||||
|
||||
<section class="st-section">
|
||||
<h2 class="st-section-title">
|
||||
<span class="st-step">3</span> {{ t('stores.uninstallTitle') }}
|
||||
</h2>
|
||||
<p class="st-section-desc">{{ t('stores.uninstallDesc') }}</p>
|
||||
<CommandBlock :command="current.uninstallCmd" />
|
||||
<p class="st-note">{{ t('stores.uninstallNote') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Below here nothing depends on the device: the cartridges, and the
|
||||
engines both stores are built from, are the same either way. -->
|
||||
<section class="st-section">
|
||||
<h2 class="st-section-title">{{ t('stores.platformsTitle') }}</h2>
|
||||
<p class="st-section-desc">{{ t('stores.platformsDesc') }}</p>
|
||||
<ul class="st-platforms">
|
||||
<li v-for="p in platforms" :key="p.platform" class="st-platform">
|
||||
<i :class="p.icon" class="st-platform-icon"></i>
|
||||
<span class="st-platform-name">{{ p.label }}</span>
|
||||
<code class="st-platform-ext">{{ p.ext }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="st-section st-engine">
|
||||
<h2 class="st-section-title">
|
||||
<i class="fa-solid fa-cubes st-engine-icon"></i> {{ t('stores.engineTitle') }}
|
||||
</h2>
|
||||
<p class="st-section-desc">{{ t('stores.engineDesc') }}</p>
|
||||
<div class="st-links">
|
||||
<a
|
||||
v-for="e in engines" :key="e.name" :href="e.url" target="_blank" rel="noopener noreferrer"
|
||||
class="st-link st-link-slate"
|
||||
>
|
||||
<i class="fa-solid fa-code-branch"></i> {{ e.name }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="st-back">
|
||||
<RouterLink to="/catalog" class="st-back-link">{{ t('catalogShow.back') }}</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CONFIG } from '../../lib/config'
|
||||
import CommandBlock from './CommandBlock.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const FORGE = 'https://git.teletypegames.org/stores'
|
||||
const ENGINES_FORGE = 'https://git.teletypegames.org/engines'
|
||||
|
||||
type DeviceId = 'batocera' | 'retroarch'
|
||||
|
||||
const BATOCERA_CLI = '/userdata/system/batocera-store/ttg-store'
|
||||
const RETROARCH_CLI = '~/.local/bin/ttg-retroarch-store'
|
||||
|
||||
const devices = [
|
||||
{
|
||||
id: 'batocera' as DeviceId,
|
||||
name: 'Batocera',
|
||||
icon: 'fa-solid fa-tv',
|
||||
projectUrl: 'https://batocera.org',
|
||||
repoUrl: `${FORGE}/ttg-batocera-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-batocera-store`,
|
||||
// The installer is served straight from the forge, so this one line is the
|
||||
// whole install on the device.
|
||||
installCmd: `curl -fsSL ${FORGE}/ttg-batocera-store/raw/branch/master/install.sh | sh`,
|
||||
afterInstallCmd: 'batocera-es-swissknife --restart',
|
||||
uninstallCmd: `curl -fsSL ${FORGE}/ttg-batocera-store/raw/branch/master/uninstall.sh | sh`,
|
||||
cliSnippet: [
|
||||
`${BATOCERA_CLI} list # compatible catalog entries`,
|
||||
`${BATOCERA_CLI} sync # download everything new`,
|
||||
`${BATOCERA_CLI} remove c64demo`,
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
id: 'retroarch' as DeviceId,
|
||||
name: 'RetroArch',
|
||||
icon: 'fa-solid fa-gamepad',
|
||||
projectUrl: 'https://www.retroarch.com',
|
||||
repoUrl: `${FORGE}/ttg-retroarch-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-retroarch-store`,
|
||||
installCmd: `curl -fsSL ${FORGE}/ttg-retroarch-store/raw/branch/master/install.sh | sh`,
|
||||
afterInstallCmd: '',
|
||||
uninstallCmd: `curl -fsSL ${FORGE}/ttg-retroarch-store/raw/branch/master/uninstall.sh | sh`,
|
||||
cliSnippet: [
|
||||
`${RETROARCH_CLI} paths # where things go, and whether the cores are there`,
|
||||
`${RETROARCH_CLI} list # compatible catalog entries`,
|
||||
`${RETROARCH_CLI} sync # download everything new`,
|
||||
`${RETROARCH_CLI} remove c64demo`,
|
||||
].join('\n'),
|
||||
},
|
||||
]
|
||||
|
||||
// `?device=` keeps a link to one of the two shareable, and lets /batocera
|
||||
// redirect here without losing which store the visitor came for.
|
||||
const initial = devices.some((d) => d.id === route.query.device)
|
||||
? (route.query.device as DeviceId)
|
||||
: 'batocera'
|
||||
const device = ref<DeviceId>(initial)
|
||||
const current = computed(() => devices.find((d) => d.id === device.value) ?? devices[0])
|
||||
|
||||
function select(id: DeviceId) {
|
||||
device.value = id
|
||||
router.replace({ query: { ...route.query, device: id } })
|
||||
}
|
||||
|
||||
const platforms = [
|
||||
{ platform: 'c64', label: 'Commodore 64 (VICE)', ext: '.prg', icon: 'fa-solid fa-floppy-disk' },
|
||||
{ platform: 'tic80', label: 'TIC-80', ext: '.tic', icon: 'fa-solid fa-tv' },
|
||||
]
|
||||
|
||||
const engines = [
|
||||
{ name: 'warpstore', url: `${ENGINES_FORGE}/warpstore` },
|
||||
{ name: 'warp-engine-batocera-store', url: `${FORGE}/warp-engine-batocera-store` },
|
||||
{ name: 'warp-engine-retroarch-store', url: `${FORGE}/warp-engine-retroarch-store` },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.st-panel {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 p-6 md:p-8;
|
||||
}
|
||||
.st-lead {
|
||||
@apply text-gray-600 leading-relaxed;
|
||||
}
|
||||
.st-links {
|
||||
@apply flex flex-wrap gap-3;
|
||||
}
|
||||
.st-link {
|
||||
@apply inline-flex items-center gap-2 font-bold py-2.5 px-5 rounded-xl text-sm text-white transition-all active:scale-95;
|
||||
}
|
||||
.st-link-dark { @apply bg-slate-800 hover:bg-slate-900 shadow-lg shadow-slate-800/20; }
|
||||
.st-link-slate { @apply bg-slate-600 hover:bg-slate-700 shadow-lg shadow-slate-600/20; }
|
||||
.st-link-indigo { @apply bg-indigo-600 hover:bg-indigo-700 shadow-lg shadow-indigo-600/20; }
|
||||
.st-link-emerald { @apply bg-emerald-600 hover:bg-emerald-700 shadow-lg shadow-emerald-600/20; }
|
||||
|
||||
.st-section {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 p-6 md:p-8 mt-6;
|
||||
}
|
||||
.st-section-title {
|
||||
@apply flex items-center gap-3 text-xl font-bold text-gray-900 mb-2;
|
||||
}
|
||||
.st-step {
|
||||
@apply inline-flex items-center justify-center w-7 h-7 rounded-full bg-emerald-100 text-emerald-700 text-sm font-bold;
|
||||
}
|
||||
.st-section-desc {
|
||||
@apply text-gray-600 mb-4;
|
||||
}
|
||||
.st-note {
|
||||
@apply text-gray-600 mt-4 mb-3 text-sm;
|
||||
}
|
||||
|
||||
.st-devices {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 gap-3 mb-6;
|
||||
}
|
||||
.st-device {
|
||||
@apply flex flex-col items-start gap-1 text-left bg-gray-50 border-2 border-gray-100 rounded-xl px-4 py-3 transition-all hover:border-emerald-200 active:scale-95;
|
||||
}
|
||||
.st-device-active {
|
||||
@apply bg-emerald-50 border-emerald-500;
|
||||
}
|
||||
.st-device-icon {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.st-device-active .st-device-icon {
|
||||
@apply text-emerald-600;
|
||||
}
|
||||
.st-device-name {
|
||||
@apply font-bold text-gray-900;
|
||||
}
|
||||
.st-device-tagline {
|
||||
@apply text-sm text-gray-500;
|
||||
}
|
||||
|
||||
.st-engine {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
.st-engine-icon {
|
||||
@apply text-gray-400 text-lg;
|
||||
}
|
||||
.st-platforms {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 gap-3;
|
||||
}
|
||||
.st-platform {
|
||||
@apply flex items-center gap-3 bg-gray-50 border border-gray-100 rounded-xl px-4 py-3;
|
||||
}
|
||||
.st-platform-icon {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.st-platform-name {
|
||||
@apply font-medium text-gray-800 flex-grow;
|
||||
}
|
||||
.st-platform-ext {
|
||||
@apply font-mono text-xs text-purple-600 bg-purple-50 px-2 py-0.5 rounded;
|
||||
}
|
||||
.st-back {
|
||||
@apply mt-8 text-center;
|
||||
}
|
||||
.st-back-link {
|
||||
@apply text-gray-400 hover:text-gray-600 text-sm font-medium transition-colors;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const batoceraRouter: RouteRecordRaw[] = [
|
||||
{ path: '/batocera', name: 'batoceraIndex', component: () => import('../page/batocera/BatoceraIndexPage.vue') },
|
||||
]
|
||||
@@ -8,7 +8,7 @@ import { enginesRouter } from './engines.router'
|
||||
import { howtosRouter } from './howtos.router'
|
||||
import { teamRouter } from './team.router'
|
||||
import { buildsRouter } from './builds.router'
|
||||
import { batoceraRouter } from './batocera.router'
|
||||
import { storesRouter } from './stores.router'
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -22,7 +22,7 @@ export const router = createRouter({
|
||||
...howtosRouter,
|
||||
...teamRouter,
|
||||
...buildsRouter,
|
||||
...batoceraRouter,
|
||||
...storesRouter,
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const storesRouter: RouteRecordRaw[] = [
|
||||
{ path: '/stores', name: 'storesIndex', component: () => import('../page/stores/StoresIndexPage.vue') },
|
||||
// The Batocera store had a page to itself until the RetroArch store joined it.
|
||||
// Both device names keep working as URLs — the old links, and the guess someone
|
||||
// makes after reading "RetroArch store" — and each lands on its own tab.
|
||||
{ path: '/batocera', redirect: { name: 'storesIndex', query: { device: 'batocera' } } },
|
||||
{ path: '/retroarch', redirect: { name: 'storesIndex', query: { device: 'retroarch' } } },
|
||||
]
|
||||
Reference in New Issue
Block a user