In-app store, torrential backend, locales (#332)

* feat: add store nav and fixes

* fix: reduce password requirement & new task error ui

* fix: client webtoken fix

* fix: delta versions and dockerfile

* fix: use setup platforms for filter & display

* fix: setup not accounted when returning valid options

* feat: tighter delta version support

* feat: dl/disk size

* feat: offload manifest generation to torrential

* fix: bump torrential

* feat: remove droplet

* feat: bump torrential

* feat: convert locales
This commit is contained in:
DecDuck
2026-02-06 00:12:24 +11:00
committed by GitHub
parent 6b614acfd8
commit 13c97cfcfc
82 changed files with 1737 additions and 967 deletions
+13 -10
View File
@@ -90,7 +90,7 @@ import {
startAuthentication,
browserSupportsWebAuthn,
} from "@simplewebauthn/browser";
import type { FetchError } from "ofetch";
import { FetchError } from "ofetch";
const username = ref("");
const password = ref("");
@@ -141,16 +141,19 @@ const router = useRouter();
const route = useRoute();
const { t } = useI18n();
function signin_wrapper() {
async function signin_wrapper() {
loading.value = true;
signin()
.catch((response) => {
const message = response.message || t("errors.unknown");
error.value = message;
})
.finally(() => {
loading.value = false;
});
try {
await signin();
} catch (e) {
if (e instanceof FetchError) {
error.value = e.data.message || t("errors.unknown");
} else {
error.value = e as string;
}
} finally {
loading.value = false;
}
}
async function signin() {
+12 -13
View File
@@ -3,9 +3,11 @@
<div v-if="game && unimportedVersions" class="px-4 sm:px-6 lg:px-8 py-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-white">Versions</h1>
<h1 class="text-base font-semibold text-white">
{{ $t("library.admin.version.title") }}
</h1>
<p class="mt-2 text-sm text-gray-300">
Versions versions version, versions versions. Versions.
{{ $t("library.admin.version.description") }}
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
@@ -36,28 +38,28 @@
scope="col"
class="py-3 pr-3 pl-4 text-left text-xs font-medium tracking-wide text-gray-400 uppercase sm:pl-0"
>
Name (ID)
{{ $t("library.admin.version.table.name") }}
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Path
{{ $t("library.admin.version.table.path") }}
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Setup Configurations
{{ $t("library.admin.version.table.setup") }}
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Launch Configurations
{{ $t("library.admin.version.table.launch") }}
</th>
<th scope="col" class="py-3 pr-4 pl-3 sm:pr-0">
<span class="sr-only">Edit</span>
<span class="sr-only">{{ $t("common.edit") }}</span>
</th>
</tr>
</thead>
@@ -100,13 +102,13 @@
v-if="version.setups.length == 0"
class="text-xs uppercase font-display text-zinc-700 font-semibold"
>
No setups configured.
{{ $t("library.admin.version.noSetups") }}
</li>
</ul>
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
<div v-if="version.onlySetup">
Version configured as in setup-only mode.
{{ $t("library.admin.version.setupOnly") }}
</div>
<ul v-else class="space-y-2">
<GameEditorVersionConfig
@@ -131,10 +133,7 @@
class="text-red-400 hover:text-red-300"
@click="() => deleteVersion(version.versionId)"
>
Delete<span class="sr-only"
>,
{{ version.displayName ?? version.versionPath }}</span
>
{{ $t("common.delete") }}
</button>
</td>
</tr></template
+22 -13
View File
@@ -26,8 +26,7 @@
<div
class="z-50 w-64 transition duration-100 opacity-0 shadow peer-hover:opacity-100 absolute left-0 p-2 bg-zinc-900 rounded text-xs text-zinc-300"
>
The installation directory is set as the current directory when
launching. It is not prepended to your command.
{{ $t("library.admin.launchRow.currentDirHint") }}
</div>
</div>
{{ $t("library.admin.import.version.installDir") }}
@@ -124,7 +123,7 @@
<span
:class="['block truncate', selected && 'font-semibold']"
>
'{{ launchProcessQuery }}'
{{ launchProcessQuery }}
</span>
<span
@@ -155,11 +154,21 @@
</div>
<div class="ml-2 inline-flex items-center">
<p class="text-sm text-blue-200">
<span
class="font-mono bg-zinc-950 text-zinc-100 py-1 px-0.5 rounded-xl"
>{executor}</span
<i18n-t
keypath="library.admin.launchRow.executorHint"
tag="span"
scope="global"
>
is replaced with the game's launch command for executors.
<template #executor>
<span
class="font-mono bg-zinc-950 text-zinc-100 py-1 px-0.5 rounded-xl"
>{{
// eslint-disable-next-line @intlify/vue-i18n/no-raw-text
"{executor}"
}}</span
>
</template>
</i18n-t>
</p>
</div>
</div>
@@ -174,7 +183,7 @@
</SelectorPlatform>
<div v-if="props.type && props.type === 'Game' && props.allowExecutor">
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
Executor
{{ $t("library.admin.launchRow.executorTitle") }}
</h1>
<div class="relative mt-2 space-x-1 inline-flex items-center w-full">
<ExecutorWidget v-if="executor" :executor="executor" />
@@ -182,12 +191,12 @@
v-else
class="font-bold uppercase font-display text-zinc-500 text-sm"
>
No executor selected
{{ $t("library.admin.launchRow.noExecutorSelected") }}
</div>
<div class="grow" />
<LoadingButton :loading="false" @click="selectLaunchOpen = true"
>Select new executor</LoadingButton
>
<LoadingButton :loading="false" @click="selectLaunchOpen = true">{{
$t("library.admin.launchRow.executorSelect")
}}</LoadingButton>
<button
:disabled="!executor"
class="transition rounded p-2 bg-zinc-900/30 group hover:enabled:bg-red-600/10 text-zinc-400 hover:enabled:text-red-600 disabled:bg-zinc-900/80 disabled:text-zinc-700"
@@ -199,7 +208,7 @@
</div>
<div v-if="props.type && props.type === 'Executor'">
<p class="block text-sm font-medium leading-6 text-zinc-100">
Auto-suggest extensions
{{ $t("library.admin.launchRow.autosuggestHint") }}
</p>
<SelectorFileExtension
v-model="launchConfiguration.suggestions!"
+2 -15
View File
@@ -59,7 +59,6 @@ const emit = defineEmits<{
const open = defineModel<boolean>({ required: true });
const { t } = useI18n();
const collectionName = ref("");
const createCollectionLoading = ref(false);
const collections = await useCollections();
@@ -74,6 +73,7 @@ async function createCollection() {
const response = await $dropFetch("/api/v1/collection", {
method: "POST",
body: { name: collectionName.value },
failTitle: "Failed to create collection",
});
// Add the game if provided
@@ -83,6 +83,7 @@ async function createCollection() {
>(`/api/v1/collection/${response.id}/entry`, {
method: "POST",
body: { id: props.gameId },
failTitle: "Failed to add game to collection",
});
response.entries.push(entry);
}
@@ -94,20 +95,6 @@ async function createCollection() {
open.value = false;
emit("created", response.id);
} catch (error) {
console.error("Failed to create collection:", error);
const err = error as { statusMessage?: string };
createModal(
ModalType.Notification,
{
title: t("errors.library.collection.create.title"),
description: t("errors.library.collection.create.desc", [
err?.statusMessage ?? t("errors.unknown"),
]),
},
(_, c) => c(),
);
} finally {
createCollectionLoading.value = false;
}
+1 -1
View File
@@ -22,7 +22,7 @@
class="bg-red-600 text-white hover:bg-red-500"
@click="() => deleteCollection()"
>
{{ $t("delete") }}
{{ $t("common.delete") }}
</LoadingButton>
<button
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
+1 -1
View File
@@ -22,7 +22,7 @@
class="bg-red-600 text-white hover:bg-red-500"
@click="() => deleteArticle()"
>
{{ $t("delete") }}
{{ $t("common.delete") }}
</LoadingButton>
<button
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
+1 -1
View File
@@ -22,7 +22,7 @@
class="bg-red-600 text-white hover:bg-red-500"
@click="() => deleteUser()"
>
{{ $t("delete") }}
{{ $t("common.delete") }}
</LoadingButton>
<button
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
+8 -8
View File
@@ -3,17 +3,17 @@
<template #default>
<div>
<h1 as="h3" class="text-lg font-medium leading-6 text-white">
Select a launch option
{{ $t("library.admin.launchSelector.title") }}
</h1>
<p class="mt-1 text-zinc-400 text-sm">
Select a launch option as an executor for your new launch option.
{{ $t("library.admin.launchSelector.description") }}
</p>
<div
v-if="props.filterPlatform"
class="inline-flex items-center mt-2 gap-x-4"
>
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
Only showing launches for:
{{ $t("library.admin.launchSelector.platformFilterHint") }}
</h1>
<span class="flex items-center">
<component
@@ -30,7 +30,7 @@
<div class="mt-2 space-y-4">
<div>
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
Search for an executor
{{ $t("library.admin.launchSelector.search") }}
</h1>
<SelectorGame
:search="search"
@@ -43,11 +43,11 @@
v-if="versions !== undefined && Object.entries(versions).length == 0"
class="text-zinc-300 text-sm font-bold font-display uppercase text-center w-full"
>
No versions imported.
{{ $t("library.admin.launchSelector.noVersions") }}
</div>
<div v-else-if="versions !== undefined">
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
Select a version
{{ $t("library.admin.launchSelector.selectVersions") }}
</h1>
<SelectorCombox
:search="
@@ -75,7 +75,7 @@
</div>
<div v-if="versions && version">
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
Select a launch command
{{ $t("library.admin.launchSelector.selectCommand") }}
</h1>
<SelectorCombox
:search="
@@ -127,7 +127,7 @@
</template>
<template #buttons>
<LoadingButton :loading="false" :disabled="!launchId" @click="submit">
Select
{{ $t("common.select") }}
</LoadingButton>
<button
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
+1 -1
View File
@@ -29,7 +29,7 @@
v-if="results.length == 0"
class="text-zinc-300 uppercase font-display font-bold text-center p-4"
>
No results.
{{ $t("common.noResults") }}
</div>
<ComboboxOption
v-for="result in results"
+9 -5
View File
@@ -12,7 +12,7 @@
class="group relative -mr-1 size-3.5 rounded-xs hover:bg-blue-500/30"
@click="() => removeFileExtension(extension)"
>
<span class="sr-only">Remove</span>
<span class="sr-only">{{ $t("common.remove") }}</span>
<svg
viewBox="0 0 14 14"
class="size-3.5 stroke-blue-400 group-hover:stroke-blue-300"
@@ -22,9 +22,9 @@
<span class="absolute -inset-1"></span>
</button>
</span>
<span v-if="model.length == 0" class="text-zinc-500 text-xs"
>No extensions selected.</span
>
<span v-if="model.length == 0" class="text-zinc-500 text-xs">{{
$t("library.admin.fileExtSelector.noSelected")
}}</span>
</div>
<Combobox
as="div"
@@ -65,7 +65,11 @@
: 'text-zinc-100',
]"
>
<span> Add "{{ normalize(query) }}" </span>
<span>
{{
$t("library.admin.fileExtSelector.add", [normalize(query)])
}}</span
>
<span
v-if="selected"
+2 -2
View File
@@ -26,7 +26,7 @@
v-if="gameSearchQuery.length < 4"
class="text-zinc-300 uppercase font-display font-bold text-center p-4"
>
Type at least 4 characters to get results
{{ $t("library.admin.gameSelector.hint") }}
</div>
<div
v-else-if="resultsLoading || results === undefined"
@@ -53,7 +53,7 @@
v-else-if="results.length == 0"
class="text-zinc-500 uppercase font-display font-bold text-center p-4"
>
No results
{{ $t("common.noResults") }}
</div>
<ComboboxOption
v-for="result in results"
+1 -1
View File
@@ -128,7 +128,7 @@
class="text-red-500 hover:text-red-400"
@click="() => deleteSource(sourceIdx)"
>
{{ $t("delete") }}
{{ $t("common.delete") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
+39
View File
@@ -0,0 +1,39 @@
<template>
<div
class="w-full bg-zinc-950 p-1 inline-flex items-center gap-x-2 fixed inset-x-0 top-0 z-100"
>
<button
class="p-1 text-zinc-300 hover:text-zinc-100 hover:bg-zinc-900 transition-all rounded"
@click="() => router.back()"
>
<ChevronLeftIcon class="size-4" />
</button>
<button
class="p-1 text-zinc-300 hover:text-zinc-100 hover:bg-zinc-900 transition-all rounded"
@click="() => router.forward()"
>
<ChevronRightIcon class="size-4" />
</button>
<span class="text-zinc-400 text-sm">
{{ title }}
</span>
</div>
</template>
<script setup lang="ts">
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/outline";
const router = useRouter();
const title = ref("Loading...");
onMounted(() => {
title.value = document.title;
});
router.afterEach(() => {
title.value = "Loading...";
// TODO: more robust after-render "detection"
setTimeout(() => {
title.value = document.title;
}, 500);
});
</script>