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:
@@ -85,7 +85,7 @@
|
||||
@click="deleteNotification(notification.id)"
|
||||
>
|
||||
<TrashIcon class="size-3" />
|
||||
{{ $t("delete") }}
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,13 +13,21 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-300">
|
||||
Sign in again to access these settings.
|
||||
{{ " " }}
|
||||
{{ $t("account.security.2fa.superlevelHint.title") }}
|
||||
<NuxtLink
|
||||
href="/auth/signin?redirect=/account/security&superlevel=true"
|
||||
class="font-medium underline text-yellow-300 hover:text-yellow-200"
|
||||
>Sign in →</NuxtLink
|
||||
>
|
||||
<i18n-t
|
||||
keypath="account.security.2fa.superlevelHint.signin"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,7 +39,7 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-300">
|
||||
You have access to these protected actions.
|
||||
{{ $t("account.security.2fa.superlevelHint.success") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +48,7 @@
|
||||
<div></div>
|
||||
<div class="mt-8 border-b border-white/10 pb-2">
|
||||
<h3 class="text-base font-semibold text-white">
|
||||
Two-factor authentication
|
||||
{{ $t("account.security.2fa.title") }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-8">
|
||||
@@ -67,15 +75,16 @@
|
||||
class="absolute inset-0"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
TOTP
|
||||
{{ $t("account.security.2fa.totp.title") }}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-400">
|
||||
TOTP generates one-time codes, completely offline. You can use any
|
||||
TOTP authenticator you like.
|
||||
{{ $t("account.security.2fa.totp.description") }}
|
||||
</p>
|
||||
<div v-if="mfa.mecs.TOTP?.enabled" class="mt-3">
|
||||
<LoadingButton :loading="false">Disable</LoadingButton>
|
||||
<LoadingButton :loading="false">{{
|
||||
$t("account.security.2fa.totp.disableButton")
|
||||
}}</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -107,20 +116,21 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8 max-w-sm">
|
||||
<h3 class="text-base font-semibold text-white">WebAuthn</h3>
|
||||
<h3 class="text-base font-semibold text-white">
|
||||
{{ $t("account.security.2fa.webauthn.title") }}
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-400">
|
||||
Otherwise known as passkeys. Authenticate using biometrics, a
|
||||
device, YubiKeys, or any compatible FIDO2 device.
|
||||
{{ $t("account.security.2fa.webauthn.description") }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs font-bold text-zinc-300">
|
||||
Also lets you bypass signing in with compatible devices.
|
||||
{{ $t("account.security.2fa.webauthn.bypassHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<LoadingButton
|
||||
class="mt-3"
|
||||
:loading="false"
|
||||
@click="() => (webAuthnOpen = true)"
|
||||
>Manage</LoadingButton
|
||||
>{{ $t("account.security.2fa.webauthn.manage") }}</LoadingButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,9 +140,11 @@
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-white">WebAuthn Keys</h1>
|
||||
<h1 class="text-base font-semibold text-white">
|
||||
{{ $t("account.security.2fa.webauthn.modal.title") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
Create new keys or remove existing keys from your account.
|
||||
{{ $t("account.security.2fa.webauthn.modal.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
@@ -140,7 +152,7 @@
|
||||
to="/mfa/setup/webauthn"
|
||||
class="block rounded-md bg-blue-500 px-3 py-2 text-center text-sm font-semibold text-white shadow-xs hover:bg-blue-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||
>
|
||||
New key
|
||||
{{ $t("account.security.2fa.webauthn.modal.new") }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,17 +168,19 @@
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
Name
|
||||
{{ $t("account.security.2fa.webauthn.modal.tableName") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-white sm:pl-0"
|
||||
>
|
||||
Created
|
||||
{{
|
||||
$t("account.security.2fa.webauthn.modal.tableCreated")
|
||||
}}
|
||||
</th>
|
||||
|
||||
<th scope="col" class="py-3.5 pr-4 pl-3 sm:pr-0">
|
||||
<span class="sr-only">Delete</span>
|
||||
<span class="sr-only">{{ $t("common.delete") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -193,9 +207,12 @@
|
||||
<td
|
||||
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0"
|
||||
>
|
||||
<a href="#" class="text-blue-400 hover:text-blue-300"
|
||||
>Delete</a
|
||||
<button
|
||||
class="text-blue-400 hover:text-blue-300"
|
||||
@click="() => deletePasskey(mec.id)"
|
||||
>
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -229,4 +246,12 @@ const superlevel = await $dropFetch("/api/v1/user/superlevel");
|
||||
const mfa = await $dropFetch("/api/v1/user/mfa");
|
||||
|
||||
const webAuthnOpen = ref(false);
|
||||
|
||||
async function deletePasskey(id: string) {
|
||||
await $dropFetch("/api/v1/user/mfa/webauthn", {
|
||||
method: "DELETE",
|
||||
body: { id },
|
||||
failTitle: "Failed to delete passkey",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
:title="t('home.admin.biggestGamesToDownload')"
|
||||
:subtitle="t('home.admin.latestVersionOnly')"
|
||||
>
|
||||
<RankingList :items="biggestGamesLatest.map(gameToRankItem)" />
|
||||
<!-- <RankingList :items="biggestGamesLatest.map(gameToRankItem)" />-->
|
||||
</TileWithLink>
|
||||
</div>
|
||||
<div class="col-span-6 lg:col-span-2">
|
||||
@@ -181,7 +181,7 @@
|
||||
:title="t('home.admin.biggestGamesOnServer')"
|
||||
:subtitle="t('home.admin.allVersionsCombined')"
|
||||
>
|
||||
<RankingList :items="biggestGamesCombined.map(gameToRankItem)" />
|
||||
<!-- <RankingList :items="biggestGamesCombined.map(gameToRankItem)" />-->
|
||||
</TileWithLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,8 +196,6 @@ import DropLogo from "~/components/DropLogo.vue";
|
||||
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
|
||||
import { getPercentage } from "~/utils/utils";
|
||||
import { getBarColor } from "~/utils/colors";
|
||||
import type { GameSize } from "~/server/internal/gamesize";
|
||||
import type { RankItem } from "~/components/RankingList.vue";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
@@ -211,20 +209,8 @@ const { t } = useI18n();
|
||||
|
||||
const systemData = useSystemData();
|
||||
|
||||
const {
|
||||
version,
|
||||
gameCount,
|
||||
sources,
|
||||
userStats,
|
||||
biggestGamesLatest,
|
||||
biggestGamesCombined,
|
||||
} = await $dropFetch("/api/v1/admin/home");
|
||||
|
||||
const gameToRankItem = (game: GameSize, rank: number): RankItem => ({
|
||||
rank: rank + 1,
|
||||
name: game.gameName,
|
||||
value: formatBytes(game.size),
|
||||
});
|
||||
const { version, gameCount, sources, userStats } =
|
||||
await $dropFetch("/api/v1/admin/home");
|
||||
|
||||
const pieChartData = [
|
||||
{
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
<span v-if="launch.name" class="text-sm font-semibold">{{
|
||||
launch.name
|
||||
}}</span>
|
||||
<span v-else class="text-sm text-zinc-500 italic"
|
||||
>No name provided.</span
|
||||
>
|
||||
<span v-else class="text-sm text-zinc-500 italic">{{
|
||||
$t("library.admin.import.version.noNameProvided")
|
||||
}}</span>
|
||||
<span class="ml-auto flex h-7 items-center">
|
||||
<PlusIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<MinusIcon v-else class="size-6" aria-hidden="true" />
|
||||
|
||||
@@ -115,13 +115,14 @@
|
||||
|
||||
<div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4">
|
||||
<fieldset>
|
||||
<legend class="text-sm/6 font-semibold text-white">Import as</legend>
|
||||
<legend class="text-sm/6 font-semibold text-white">
|
||||
{{ $t("library.admin.import.importAs") }}
|
||||
</legend>
|
||||
<div class="mt-6 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-4">
|
||||
<label
|
||||
v-for="[type, meta] in Object.entries(importModes)"
|
||||
:key="type"
|
||||
:aria-label="meta.title"
|
||||
:aria-description="`Import as ${meta.title}`"
|
||||
class="cursor-pointer group relative flex rounded-lg border border-white/10 bg-gray-800/50 p-4 has-checked:bg-blue-500/10 has-checked:outline-2 has-checked:-outline-offset-2 has-checked:outline-blue-500 has-focus-visible:outline-3 has-focus-visible:-outline-offset-1 has-disabled:bg-gray-800 has-disabled:opacity-25"
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
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)"
|
||||
>
|
||||
{{ $t("delete") }}
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
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="() => deleteCompany(company.id)"
|
||||
>
|
||||
{{ $t("delete") }}
|
||||
{{ $t("common.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,39 +11,35 @@
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
|
||||
<div
|
||||
v-if="task && task.error"
|
||||
class="grow w-full flex items-center justify-center"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<ExclamationCircleIcon
|
||||
class="h-12 w-12 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1
|
||||
class="text-3xl font-semibold font-display leading-6 text-zinc-100"
|
||||
>
|
||||
{{ task.error.title }}
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-md">
|
||||
{{ task.error.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="task" class="flex flex-col w-full gap-y-4">
|
||||
<div v-if="task" class="flex flex-col w-full gap-y-4">
|
||||
<h1
|
||||
class="inline-flex items-center gap-x-3 text-3xl text-zinc-100 font-bold font-display"
|
||||
>
|
||||
<div>
|
||||
<CheckCircleIcon v-if="task.success" class="size-5 text-green-600" />
|
||||
<CheckCircleIcon v-if="task.success" class="size-8 text-green-600" />
|
||||
<XMarkIcon v-else-if="task.error" class="size-8 text-red-600" />
|
||||
<div v-else class="size-4 bg-blue-600 rounded-full animate-pulse" />
|
||||
</div>
|
||||
{{ task.name }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="task.error"
|
||||
class="rounded-md bg-red-500/15 p-4 outline outline-red-500/25"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<XCircleIcon class="size-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-200">
|
||||
{{ task.error.title }}
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-200/80">
|
||||
{{ task.error.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="flex flex-row items-center h-12 gap-x-3">
|
||||
<li
|
||||
v-for="[name, link] in task.actions.map((v) => v.split(':'))"
|
||||
@@ -57,7 +53,7 @@
|
||||
v-if="task.actions.length == 0"
|
||||
class="text-md uppercase font-display font-bold text-zinc-700"
|
||||
>
|
||||
No actions
|
||||
{{ $t("tasks.admin.noActions") }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -95,8 +91,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ExclamationCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { CheckCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { XMarkIcon, XCircleIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const route = useRoute();
|
||||
const taskId = route.params.id.toString();
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
<h1
|
||||
class="mt-4 text-3xl font-semibold tracking-tight text-balance text-white sm:text-4xl"
|
||||
>
|
||||
Two-factor authentication
|
||||
{{ $t("auth.2fa.title") }}
|
||||
</h1>
|
||||
<p class="mt-6 text-sm font-medium text-pretty text-zinc-400 sm:text-md">
|
||||
Two-factor authentication is enabled on your account. Choose one of the
|
||||
options below to continue.
|
||||
{{ $t("auth.2fa.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto mt-16 flow-root max-w-lg sm:mt-20">
|
||||
@@ -18,7 +17,11 @@
|
||||
<NuxtLink
|
||||
:href="{ path: '/auth/mfa', query: route.query }"
|
||||
class="text-sm/6 font-semibold text-blue-400"
|
||||
><span aria-hidden="true">←</span> Back to options</NuxtLink
|
||||
><i18n-t keypath="auth.2fa.backToOptions" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrowBack") }}</span>
|
||||
</template>
|
||||
</i18n-t></NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
<h3 class="text-sm/6 font-semibold text-white">
|
||||
<NuxtLink :to="{ path: '/auth/mfa/totp', query: route.query }">
|
||||
<span class="absolute inset-0" aria-hidden="true"></span>
|
||||
TOTP
|
||||
{{ $t("auth.2fa.totp.title") }}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm/6 text-zinc-400">
|
||||
Use a one-time code to sign in to your Drop account.
|
||||
{{ $t("auth.2fa.totp.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-none self-center">
|
||||
@@ -34,12 +34,11 @@
|
||||
<h3 class="text-sm/6 font-semibold text-white">
|
||||
<NuxtLink :to="{ path: '/auth/mfa/webauthn', query: route.query }">
|
||||
<span class="absolute inset-0" aria-hidden="true"></span>
|
||||
WebAuthn
|
||||
{{ $t("auth.2fa.passkey.title") }}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm/6 text-zinc-400">
|
||||
Use a passkey, like biometrics, a hardware security device, or other
|
||||
compatible device to sign in to your Drop account.
|
||||
{{ $t("auth.2fa.passkey.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-none self-center">
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
<div v-else class="inline-flex gap-x-2">
|
||||
<LoadingButton :loading="false" @click="() => tryAuthWrapper()">
|
||||
Sign in with WebAuthn</LoadingButton
|
||||
>
|
||||
{{ $t("auth.2fa.passkey.signinButton") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-8 rounded-md bg-red-600/10 p-4">
|
||||
|
||||
@@ -227,7 +227,7 @@ const validUsername = computed(
|
||||
!((usernameValidator(username.value) as unknown) instanceof type.errors),
|
||||
);
|
||||
|
||||
const passwordValidator = type("string >= 14");
|
||||
const passwordValidator = type("string >= 8");
|
||||
const validPassword = computed(
|
||||
() =>
|
||||
!((passwordValidator(password.value) as unknown) instanceof type.errors),
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
>
|
||||
{{
|
||||
superlevel
|
||||
? "Sign in to access protected action"
|
||||
? $t("auth.signin.titleProtected")
|
||||
: $t("auth.signin.title")
|
||||
}}
|
||||
</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-zinc-400">
|
||||
{{
|
||||
superlevel
|
||||
? "We need you to sign in again for security reasons while attempting to access more sensitive actions."
|
||||
? $t("auth.signin.noAccountProtected")
|
||||
: $t("auth.signin.noAccount")
|
||||
}}
|
||||
</p>
|
||||
|
||||
@@ -8,21 +8,26 @@ import { CheckCircleIcon } from "@heroicons/vue/24/outline";
|
||||
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Added your 2FA method!
|
||||
{{ $t("auth.2fa.success.title") }}
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="mx-auto text-sm text-zinc-400 max-w-sm">
|
||||
Drop has successfully created and added your 2FA method. If this is
|
||||
your first time configuring 2FA, your account now requires it to
|
||||
sign in.
|
||||
{{ $t("auth.2fa.success.description") }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex justify-center">
|
||||
<NuxtLink
|
||||
href="/account/security"
|
||||
class="text-sm/6 font-semibold text-blue-400"
|
||||
><span aria-hidden="true">←</span> Back to account
|
||||
security</NuxtLink
|
||||
><i18n-t
|
||||
keypath="auth.2fa.success.back"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrowBack") }}</span>
|
||||
</template>
|
||||
</i18n-t></NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
<h1
|
||||
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
|
||||
>
|
||||
Set up your authenticator
|
||||
{{ $t("auth.2fa.totp.createTitle") }}
|
||||
</h1>
|
||||
<p class="mt-6 text-base leading-7 text-zinc-400">
|
||||
Use your TOTP authenticator, like Google Authenticator, Aegis, or
|
||||
Bitwarden, to add 2FA to your Drop account.
|
||||
{{ $t("auth.2fa.totp.createDescription") }}
|
||||
</p>
|
||||
<div class="mt-8">
|
||||
<p class="text-xs leading-7 text-zinc-200">
|
||||
Enter the generated code to enable TOTP
|
||||
{{ $t("auth.2fa.totp.createHint") }}
|
||||
</p>
|
||||
<div class="mt-2 flex flex-row gap-2">
|
||||
<CodeInput
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
<h2
|
||||
class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-white"
|
||||
>
|
||||
Create a passkey
|
||||
{{ $t("auth.2fa.passkey.createTitle") }}
|
||||
</h2>
|
||||
<p class="text-sm text-center text-zinc-400">
|
||||
WebAuthn, or passkeys, allow you to sign in or complete 2FA with
|
||||
biometrics or hardware security devices.
|
||||
{{ $t("auth.2fa.passkey.createDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +22,9 @@
|
||||
@submit.prevent="attemptPasskeyWrapper"
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block text-sm/6 font-medium text-gray-100"
|
||||
>Name</label
|
||||
>
|
||||
<label for="name" class="block text-sm/6 font-medium text-gray-100">{{
|
||||
$t("auth.2fa.passkey.passkeyNameTag")
|
||||
}}</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="name"
|
||||
@@ -41,7 +40,7 @@
|
||||
|
||||
<div>
|
||||
<LoadingButton :disabled="disabled" :loading="loading" class="w-full">
|
||||
Create
|
||||
{{ $t("common.create") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<AddLibraryButton :game-id="game.id" />
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-if="user?.admin"
|
||||
v-if="user?.admin && !isClient"
|
||||
:href="`/admin/library/${game.id}`"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 duration-200 hover:scale-105 active:scale-95"
|
||||
@@ -93,10 +93,26 @@
|
||||
{{ $t("store.size") }}
|
||||
</td>
|
||||
<td
|
||||
v-if="size"
|
||||
v-if="size.versions.length > 0"
|
||||
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400"
|
||||
>
|
||||
{{ formatBytes(size) }}
|
||||
<ul class="flex flex-col">
|
||||
<ol
|
||||
v-for="version in size.versions"
|
||||
:key="version.versionId"
|
||||
class="inline-flex items-center gap-x-1"
|
||||
>
|
||||
<ServerIcon class="size-4" />
|
||||
{{
|
||||
formatBytes(version.installSize)
|
||||
}}
|
||||
|
||||
<CloudIcon class="size-4 ml-3" />
|
||||
{{
|
||||
formatBytes(version.downloadSize)
|
||||
}}
|
||||
</ol>
|
||||
</ul>
|
||||
</td>
|
||||
<td
|
||||
v-else
|
||||
@@ -243,7 +259,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
import { StarIcon } from "@heroicons/vue/24/solid";
|
||||
import { StarIcon, ServerIcon, CloudIcon } from "@heroicons/vue/24/solid";
|
||||
import { micromark } from "micromark";
|
||||
import { formatBytes } from "~/server/internal/utils/files";
|
||||
|
||||
@@ -254,10 +270,15 @@ const user = useUser();
|
||||
|
||||
const { game, rating, size } = await $dropFetch(`/api/v1/games/${gameId}`);
|
||||
|
||||
const isClient = isClientRequest();
|
||||
|
||||
const descriptionHTML = micromark(game.mDescription);
|
||||
|
||||
const platforms = game.versions
|
||||
.map((e) => e.launches.map((v) => v.platform))
|
||||
.map((e) => [
|
||||
...e.launches.map((v) => v.platform),
|
||||
...e.setups.map((v) => v.platform),
|
||||
])
|
||||
.flat()
|
||||
.flat()
|
||||
.filter((e, i, u) => u.indexOf(e) === i);
|
||||
|
||||
Reference in New Issue
Block a user