i18n Support and Task improvements (#80)

* fix: release workflow

* feat: move mostly to internal tasks system

* feat: migrate object clean to new task system

* fix: release not  getting good base version

* chore: set version v0.3.0

* chore: style

* feat: basic task concurrency

* feat: temp pages to fill in page links

* feat: inital i18n support

* feat: localize store page

* chore: style

* fix: weblate doesn't like multifile thing

* fix: update nuxt

* feat: improved error logging

* fix: using old task api

* feat: basic translation docs

* feat: add i18n eslint plugin

* feat: translate store and auth pages

* feat: more translation progress

* feat: admin dash i18n progress

* feat: enable update check by default in prod

* fix: using wrong i18n keys

* fix: crash in library sources page

* feat: finish i18n work

* fix: missing i18n translations

* feat: use twemoji for emojis

* feat: sanatize object ids

* fix: EmojiText's alt text

* fix: UserWidget not using links

* feat: cache and auth for emoji api

* fix: add more missing translations
This commit is contained in:
Husky
2025-06-04 19:53:30 -04:00
committed by GitHub
parent c7fab132ab
commit 681efe95af
86 changed files with 5175 additions and 2816 deletions

View File

@ -5,9 +5,9 @@
:model-value="currentlySelectedVersion"
@update:model-value="(value) => updateCurrentlySelectedVersion(value)"
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
>Select version to import</ListboxLabel
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">{{
$t("library.admin.import.version.version")
}}</ListboxLabel>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
@ -15,9 +15,9 @@
<span v-if="currentlySelectedVersion != -1" class="block truncate">{{
versions[currentlySelectedVersion]
}}</span>
<span v-else class="block truncate text-zinc-600"
>Please select a directory...</span
>
<span v-else class="block truncate text-zinc-600">{{
$t("library.admin.import.selectDir")
}}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
@ -79,17 +79,20 @@
<label
for="startup"
class="block text-sm font-medium leading-6 text-zinc-100"
>Setup executable/command</label
>{{ $t("library.admin.import.version.setupCmd") }}</label
>
<p class="text-zinc-400 text-xs">Ran once when the game is installed</p>
<p class="text-zinc-400 text-xs">
{{ $t("library.admin.import.version.setupDesc") }}
</p>
<div class="mt-2">
<div
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
>
<span
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
>(install_dir)/</span
>
{{ $t("library.admin.import.version.installDir") }}
</span>
<Combobox
as="div"
:value="versionSettings.setup"
@ -99,7 +102,9 @@
<div class="relative">
<ComboboxInput
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
:placeholder="'setup.exe'"
:placeholder="
$t('library.admin.import.version.setupPlaceholder')
"
@change="setupProcessQuery = $event.target.value"
@blur="setupProcessQuery = ''"
/>
@ -171,7 +176,7 @@
<span
:class="['block truncate', selected && 'font-semibold']"
>
"{{ setupProcessQuery }}"
{{ $t("chars.quoted", { text: setupProcessQuery }) }}
</span>
<span
@ -206,14 +211,11 @@
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>Setup mode</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400"
>When enabled, this version does not have a launch command, and
simply runs the executable on the user's computer. Useful for games
that only distribute installer and not portable
files.</SwitchDescription
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
$t("library.admin.import.version.setupModeDesc")
}}</SwitchDescription>
</span>
<Switch
v-model="versionSettings.onlySetup"
@ -235,16 +237,18 @@
<label
for="startup"
class="block text-sm font-medium leading-6 text-zinc-100"
>Launch executable/command</label
>{{ $t("library.admin.import.version.launchCmd") }}</label
>
<p class="text-zinc-400 text-xs">Executable to launch the game</p>
<p class="text-zinc-400 text-xs">
{{ $t("library.admin.import.version.launchDesc") }}
</p>
<div class="mt-2">
<div
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
>
<span
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
>(install_dir)/</span
>{{ $t("library.admin.import.version.installDir") }}</span
>
<Combobox
as="div"
@ -255,7 +259,9 @@
<div class="relative">
<ComboboxInput
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
:placeholder="'game.exe'"
:placeholder="
$t('library.admin.import.version.launchPlaceholder')
"
@change="launchProcessQuery = $event.target.value"
@blur="launchProcessQuery = ''"
/>
@ -327,7 +333,7 @@
<span
:class="['block truncate', selected && 'font-semibold']"
>
"{{ launchProcessQuery }}"
{{ $t("chars.quoted", { text: launchProcessQuery }) }}
</span>
<span
@ -361,7 +367,7 @@
</div>
<PlatformSelector v-model="versionSettings.platform">
Version platform
{{ $t("library.admin.import.version.platform") }}
</PlatformSelector>
<SwitchGroup as="div" class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
@ -369,13 +375,12 @@
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>Update mode</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400"
>When enabled, these files will be installed on top of (overwriting)
the previous version's. If multiple "update modes" are chained
together, they are applied in order.</SwitchDescription
>
{{ $t("library.admin.import.version.updateMode") }}
</SwitchLabel>
<SwitchDescription as="span" class="text-sm text-zinc-400">
{{ $t("library.admin.import.version.updateModeDesc") }}
</SwitchDescription>
</span>
<Switch
v-model="versionSettings.delta"
@ -398,7 +403,9 @@
<DisclosureButton
class="border-b border-zinc-600 pb-2 flex w-full items-start justify-between text-left text-zinc-100"
>
<span class="text-base/7 font-semibold">Advanced options</span>
<span class="text-base/7 font-semibold">
{{ $t("library.admin.import.version.advancedOptions") }}
</span>
<span class="ml-6 flex h-7 items-center">
<ChevronUpIcon v-if="!open" class="size-6" aria-hidden="true" />
<ChevronDownIcon v-else class="size-6" aria-hidden="true" />
@ -420,13 +427,12 @@
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>Override UMU Launcher Game ID</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400"
>By default, Drop uses a non-ID when launching with UMU
Launcher. In order to get the right patches for some games,
you may have to manually set this field.</SwitchDescription
>
{{ $t("library.admin.import.version.umuOverride") }}
</SwitchLabel>
<SwitchDescription as="span" class="text-sm text-zinc-400">
{{ $t("library.admin.import.version.umuOverrideDesc") }}
</SwitchDescription>
</span>
<Switch
v-model="umuIdEnabled"
@ -448,8 +454,9 @@
<label
for="umu-id"
class="block text-sm font-medium leading-6 text-zinc-100"
>UMU Launcher ID</label
>
{{ $t("library.admin.import.version.umuLauncherId") }}
</label>
<div class="mt-2">
<input
id="umu-id"
@ -467,7 +474,7 @@
</div>
<div v-else class="text-zinc-400">
No advanced options for this configuration.
{{ $t("library.admin.import.version.noAdv") }}
</div>
</DisclosurePanel>
</Disclosure>
@ -477,7 +484,7 @@
:loading="importLoading"
@click="startImport_wrapper"
>
Import
{{ $t("library.admin.import.import") }}
</LoadingButton>
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
<div class="flex">
@ -497,7 +504,7 @@
role="status"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
>
Loading version metadata...
{{ $t("library.admin.import.version.loadingVersion") }}
<svg
aria-hidden="true"
class="w-6 h-6 text-transparent animate-spin fill-white"
@ -514,7 +521,6 @@
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
</div>
</template>
@ -548,7 +554,7 @@ definePageMeta({
});
const router = useRouter();
const { t } = useI18n();
const route = useRoute();
const gameId = route.params.id.toString();
const versions = await $dropFetch(
@ -661,7 +667,7 @@ function startImport_wrapper() {
importLoading.value = true;
startImport()
.catch((error) => {
importError.value = error.statusMessage ?? "An unknown error occurred.";
importError.value = error.statusMessage ?? t("errors.unknown");
})
.finally(() => {
importLoading.value = false;

View File

@ -28,7 +28,7 @@
type="button"
class="inline-flex w-fit items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Open in Metadata
{{ $t("library.admin.openInMetadata") }}
<ArrowTopRightOnSquareIcon
class="-mr-0.5 h-7 w-7 p-1"
aria-hidden="true"
@ -40,7 +40,7 @@
type="button"
class="inline-flex w-fit items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Open in Store
{{ $t("library.admin.openStore") }}
<ArrowTopRightOnSquareIcon
class="-mr-0.5 h-7 w-7 p-1"
aria-hidden="true"
@ -59,7 +59,7 @@
<h3
class="text-base font-semibold font-display leading-6 text-zinc-100"
>
Version priority
{{ $t("library.admin.versionPriority") }}
<!-- import games button -->
@ -80,8 +80,8 @@
>
{{
unimportedVersions.length > 0
? "Import version"
: "No versions to import"
? $t("library.admin.import.version.import")
: $t("library.admin.import.version.noVersions")
}}
</NuxtLink>
</h3>
@ -89,7 +89,7 @@
</div>
<div class="mt-4 text-center w-full text-sm text-zinc-600">
lowest
{{ $t("lowest") }}
</div>
<draggable
:list="game.versions"
@ -105,7 +105,11 @@
{{ item.versionName }}
</div>
<div class="text-zinc-400">
{{ item.delta ? "Upgrade mode" : "" }}
{{
item.delta
? $t("library.admin.import.version.updateMode")
: ""
}}
</div>
<div class="inline-flex items-center gap-x-2">
<component
@ -126,10 +130,10 @@
v-if="game.versions.length == 0"
class="text-center font-bold text-zinc-400 my-3"
>
no versions added
{{ $t("library.admin.noVersionsAdded") }}
</div>
<div class="mt-2 text-center w-full text-sm text-zinc-600">
highest
{{ $t("highest") }}
</div>
</div>
</div>
@ -151,6 +155,8 @@ definePageMeta({
layout: "admin",
});
const { t } = useI18n();
// TODO implement UI for this
const route = useRoute();
@ -174,12 +180,12 @@ async function updateVersionOrder() {
createModal(
ModalType.Notification,
{
title: "There an error while updating the version order",
description: `Drop encountered an error while updating the version: ${
title: t("errors.version.order.title"),
description: t("errors.version.order.desc", {
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
}`,
buttonText: "Close",
error: e?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("close"),
},
(e, c) => c(),
);
@ -203,12 +209,12 @@ async function deleteVersion(versionName: string) {
createModal(
ModalType.Notification,
{
title: "There an error while deleting the version",
description: `Drop encountered an error while deleting the version: ${
title: t("errors.version.delete.title"),
description: t("errors.version.delete.desc", {
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
}`,
buttonText: "Close",
error: e?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("close"),
},
(e, c) => c(),
);

View File

@ -5,9 +5,9 @@
:model="currentlySelectedGame"
@update:model-value="(value) => updateSelectedGame_wrapper(value)"
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
>Select game to import</ListboxLabel
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">
{{ $t("library.admin.import.selectGame") }}
</ListboxLabel>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
@ -15,9 +15,9 @@
<span v-if="currentlySelectedGame != -1" class="block truncate">{{
games.unimportedGames[currentlySelectedGame].game
}}</span>
<span v-else class="block truncate text-zinc-400"
>Please select a directory...</span
>
<span v-else class="block truncate text-zinc-400">{{
$t("library.admin.import.selectDir")
}}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
@ -80,7 +80,8 @@
class="w-fit"
:loading="importLoading"
@click="() => importGame_wrapper(false)"
>Import without metadata
>
{{ $t("library.admin.import.withoutMetadata") }}
</LoadingButton>
</div>
@ -89,7 +90,7 @@
class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold"
>
<div class="h-[1px] grow bg-zinc-800" />
OR
{{ $t("auth.signin.or") }}
<div class="h-[1px] grow bg-zinc-800" />
</div>
@ -100,7 +101,7 @@
<label
for="searchTerm"
class="block text-sm/6 font-medium text-zinc-100"
>Search</label
>{{ $t("library.admin.import.search") }}</label
>
<div class="mt-2 flex">
<div class="-mr-px grid grow grid-cols-1 focus-within:relative">
@ -110,7 +111,7 @@
type="text"
name="searchTerm"
class="col-start-1 row-start-1 block w-full rounded-l-md bg-zinc-950 py-1.5 px-3 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
placeholder="John Smith"
:placeholder="$t('library.admin.import.searchPlaceholder')"
/>
</div>
<LoadingButton
@ -123,7 +124,7 @@
class="-ml-0.5 size-4 text-gray-400"
aria-hidden="true"
/>
Search
{{ $t("library.admin.import.search") }}
</LoadingButton>
</div>
</form>
@ -135,7 +136,7 @@
>
<ListboxLabel
class="block text-sm font-medium leading-6 text-zinc-100"
>Select game</ListboxLabel
>{{ $t("library.admin.import.selectGameSearch") }}</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
@ -145,9 +146,9 @@
v-if="currentlySelectedMetadata != -1"
:game="metadataResults[currentlySelectedMetadata]"
/>
<span v-else class="block truncate text-zinc-600"
>Please select a game...</span
>
<span v-else class="block truncate text-zinc-600">
{{ $t("library.admin.import.selectGamePlaceholder") }}
</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
@ -191,7 +192,7 @@
role="status"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
>
Loading game results...
{{ $t("library.admin.import.loading") }}
<svg
aria-hidden="true"
class="w-6 h-6 text-transparent animate-spin fill-white"
@ -208,7 +209,6 @@
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
<div
@ -233,7 +233,8 @@
:loading="importLoading"
:disabled="currentlySelectedMetadata === -1"
@click="() => importGame_wrapper()"
>Import
>
{{ $t("library.admin.import.import") }}
</LoadingButton>
<div
@ -274,6 +275,8 @@ definePageMeta({
layout: "admin",
});
const { t } = useI18n();
const games = await $dropFetch("/api/v1/admin/import/game");
const currentlySelectedGame = ref(-1);
const gameSearchResultsLoading = ref(false);
@ -308,8 +311,7 @@ function updateSelectedGame_wrapper(value: number) {
gameSearchResultsLoading.value = true;
updateSelectedGame(value)
.catch((error) => {
gameSearchResultsError.value =
error.statusMessage || "An unknown error occurred";
gameSearchResultsError.value = error.statusMessage || t("errors.unknown");
})
.finally(() => {
gameSearchResultsLoading.value = false;
@ -348,7 +350,7 @@ function importGame_wrapper(metadata = true) {
importError.value = undefined;
importGame(metadata)
.catch((error) => {
importError.value = error?.statusMessage || "An unknown error occurred.";
importError.value = error?.statusMessage || t("errors.unknown");
})
.finally(() => {
importLoading.value = false;

View File

@ -2,11 +2,11 @@
<div class="space-y-4">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-zinc-100">Game Library</h1>
<h1 class="text-base font-semibold text-zinc-100">
{{ $t("library.admin.gameLibrary") }}
</h1>
<p class="mt-2 text-sm text-zinc-400">
As you add folders to your library sources, Drop will detect it and
prompt you to import it. Each game needs to be imported before you can
import a version.
{{ $t("library.admin.subheader") }}
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
@ -14,7 +14,11 @@
to="/admin/library/sources"
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Sources &rarr;
<i18n-t keypath="library.admin.sources.link" tag="span">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
</div>
</div>
@ -28,15 +32,18 @@
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm text-blue-400">
Drop has detected you have new games to import.
{{ $t("library.admin.detectedGame") }}
</p>
<p class="mt-3 text-sm md:ml-6 md:mt-0">
<NuxtLink
href="/admin/library/import"
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
>
Import
<span aria-hidden="true"> &rarr;</span>
<i18n-t keypath="library.admin.import.link" tag="span">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
</p>
</div>
@ -49,7 +56,7 @@
type="text"
name="search"
class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6"
placeholder="Search library..."
:placeholder="$t('library.search')"
/>
<MagnifyingGlassIcon
class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-zinc-400 sm:size-4"
@ -80,30 +87,40 @@
>
</h3>
<dl class="mt-1 flex flex-col justify-between">
<dt class="sr-only">Short Description</dt>
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
<dd class="text-sm text-zinc-400">
{{ game.mShortDescription }}
</dd>
<dt class="sr-only">Metadata provider</dt>
<dt class="sr-only">
{{ $t("library.admin.metadataProvider") }}
</dt>
</dl>
<div class="mt-4 flex flex-col gap-y-1">
<NuxtLink
:href="`/admin/library/${game.id}`"
class="w-fit rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Open with Library &rarr;
<i18n-t keypath="library.admin.openLibrary" tag="span">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
<NuxtLink
:href="`/admin/metadata/games/${game.id}`"
class="w-fit rounded-md bg-zinc-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
>
Open with Metadata &rarr;
<i18n-t keypath="library.admin.openMetadata" tag="span">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
<button
class="w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-red-500 hover:scale-105 hover:shadow-lg hover:shadow-red-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
@click="() => deleteGame(game.id)"
>
Delete
{{ $t("delete") }}
</button>
</div>
</div>
@ -122,15 +139,18 @@
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm text-blue-400">
Drop has detected you have new verions of this game to import.
{{ $t("library.admin.detectedVersion") }}
</p>
<p class="mt-3 text-sm md:ml-6 md:mt-0">
<NuxtLink
:href="`/admin/library/${game.id}/import`"
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
>
Import
<span aria-hidden="true"> &rarr;</span>
<i18n-t keypath="library.admin.import.link" tag="span">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
</p>
</div>
@ -149,7 +169,7 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-600">
You have no versions of this game available.
{{ $t("library.admin.noVersions") }}
</h3>
</div>
</div>
@ -160,13 +180,13 @@
v-if="filteredLibraryGames.length == 0 && libraryGames.length != 0"
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
>
No results
{{ $t("common.noResults") }}
</p>
<p
v-if="filteredLibraryGames.length == 0 && libraryGames.length == 0"
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
>
No games imported
{{ $t("library.admin.noGames") }}
</p>
</ul>
</div>
@ -176,12 +196,15 @@
import { ExclamationTriangleIcon } from "@heroicons/vue/16/solid";
import { InformationCircleIcon } from "@heroicons/vue/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
const { t } = useI18n();
definePageMeta({
layout: "admin",
});
useHead({
title: "Libraries",
title: t("library.admin.title"),
});
const searchQuery = ref("");

View File

@ -2,10 +2,11 @@
<div>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-zinc-100">Library Sources</h1>
<h1 class="text-base font-semibold text-zinc-100">
{{ $t("library.admin.sources.sources") }}
</h1>
<p class="mt-2 text-sm text-zinc-400">
Configure your library sources, where Drop will look for new games and
versions to import.
{{ $t("library.admin.sources.desc") }}
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
@ -13,7 +14,7 @@
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="() => (actionSourceOpen = true)"
>
Create
{{ $t("create") }}
</button>
</div>
</div>
@ -27,28 +28,28 @@
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
>
Name
{{ $t("name") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Type
{{ $t("type") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Working?
{{ $t("library.admin.sources.working") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Options
{{ $t("options") }}
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-3">
<span class="sr-only">Edit</span>
<span class="sr-only">{{ $t("edit") }}</span>
</th>
</tr>
</thead>
@ -83,14 +84,20 @@
class="text-blue-500 hover:text-blue-400"
@click="() => edit(sourceIdx)"
>
Edit<span class="sr-only">, {{ source.name }}</span>
{{ $t("edit") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
<button
class="text-red-500 hover:text-red-400"
@click="() => deleteSource(sourceIdx)"
>
Delete<span class="sr-only">, {{ source.name }}</span>
{{ $t("delete") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
</td>
</tr>
@ -104,11 +111,10 @@
<template #default>
<div>
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-white">
Create source
{{ $t("library.admin.sources.create") }}
</DialogTitle>
<p class="mt-1 text-zinc-400 text-sm">
Drop will use this source to access your game library, and make them
available.
{{ $t("library.admin.sources.createDesc") }}
</p>
</div>
<form
@ -119,10 +125,10 @@
<label
for="name"
class="block text-sm font-medium leading-6 text-zinc-100"
>Name</label
>{{ $t("name") }}</label
>
<p class="text-zinc-400 block text-xs font-medium leading-6">
The name of your source, for reference.
{{ $t("library.admin.sources.nameDesc") }}
</p>
<div class="mt-2">
<input
@ -131,21 +137,23 @@
name="name"
type="text"
autocomplete="name"
placeholder="My New Source"
:placeholder="$t('library.admin.sources.namePlaceholder')"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div v-if="createMode">
<label class="block text-sm font-medium leading-6 text-zinc-100"
>Type</label
>
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
$t("type")
}}</label>
<p class="text-zinc-400 block text-xs font-medium leading-6">
The type of your source. Changes the required options.
{{ $t("library.admin.sources.typeDesc") }}
</p>
<RadioGroup v-model="currentSourceOption" class="mt-2">
<RadioGroupLabel class="sr-only">Type</RadioGroupLabel>
<RadioGroupLabel class="sr-only">{{
$t("type")
}}</RadioGroupLabel>
<div class="space-y-4">
<RadioGroupOption
v-for="[source, metadata] in optionsMetadataIter"
@ -220,7 +228,7 @@
class="w-full sm:w-fit"
@click="() => performActionSource_wrapper()"
>
{{ createMode ? "Create" : "Save" }}
{{ createMode ? $t("create") : $t("save") }}
</LoadingButton>
<button
ref="cancelButtonRef"
@ -233,7 +241,7 @@
}
"
>
Cancel
{{ $t("cancel") }}
</button>
</template>
</ModalTemplate>
@ -268,6 +276,8 @@ definePageMeta({
layout: "admin",
});
const { t } = useI18n();
const sources = ref(
await $dropFetch<WorkingLibrarySource[]>("/api/v1/admin/library/sources"),
);
@ -293,8 +303,7 @@ const optionsMetadata: {
};
} = {
Filesystem: {
description:
"Imports games from a path on disk. Requires version-based folder structure, and supports archived games.",
description: t("library.admin.sources.fsDesc"),
icon: DocumentIcon,
},
};
@ -367,9 +376,11 @@ async function deleteSource(index: number) {
createModal(
ModalType.Notification,
{
title: "Failed to delete library source",
// @ts-expect-error attempt to display statusMessage on error
description: `Drop couldn't add delete this source: ${e?.statusMessage}`,
title: t("errors.library.source.delete.title"),
description: t("errors.library.source.delete.desc", [
// @ts-expect-error attempt to display statusMessage on error
e?.statusMessage ?? t("errors.unknown"),
]),
},
(_, c) => c(),
);

View File

@ -26,7 +26,7 @@
class="relative inline-flex gap-x-3 items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="() => (showEditCoreMetadata = true)"
>
Edit <PencilIcon class="size-4" />
{{ $t("edit") }} <PencilIcon class="size-4" />
</button>
</div>
@ -597,7 +597,7 @@ watch(descriptionHTML, (_v) => {
title: "Failed to update game description",
description: `Drop failed to update the game description: ${
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred."
e?.statusMessage ?? t("errors.unknown")
}`,
buttonText: "Close",
},
@ -642,7 +642,7 @@ async function updateBannerImage(id: string) {
title: "There an error while updating the banner image",
description: `Drop encountered an error while updating the banner image: ${
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
e?.statusMessage ?? t("errors.unknown")
}`,
buttonText: "Close",
},
@ -670,7 +670,7 @@ async function updateCoverImage(id: string) {
title: "There an error while updating the cover image",
description: `Drop encountered an error while updating the cover image: ${
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
e?.statusMessage ?? t("errors.unknown")
}`,
buttonText: "Close",
},
@ -700,7 +700,7 @@ async function deleteImage(id: string) {
title: "There an error while deleting the image",
description: `Drop encountered an error while deleting the image: ${
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
e?.statusMessage ?? t("errors.unknown")
}`,
buttonText: "Close",
},
@ -743,7 +743,7 @@ async function updateImageCarousel() {
title: "There an error while updating the image carousel",
description: `Drop encountered an error while updating image carousel: ${
// @ts-expect-error attempt to get statusMessage on error
e?.statusMessage ?? "An unknown error occurred"
e?.statusMessage ?? t("errors.unknown")
}`,
buttonText: "Close",
},

View File

@ -0,0 +1,12 @@
<template>
<div class="text-gray-100">Todo page</div>
</template>
<script lang="ts" setup>
useHead({
title: "Settings",
});
definePageMeta({
layout: "admin",
});
</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="text-gray-100">Todo page</div>
</template>
<script lang="ts" setup>
useHead({
title: "Tasks",
});
definePageMeta({
layout: "admin",
});
</script>

View File

@ -2,7 +2,9 @@
<div>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-zinc-100">Users</h1>
<h1 class="text-base font-semibold text-zinc-100">
{{ $t("header.admin.users") }}
</h1>
<p class="mt-2 text-sm text-zinc-400">
Manage the users on your Drop instance, and configure your
authentication methods.