feat: partial user platform support + statusMessage -> message

This commit is contained in:
DecDuck
2025-08-27 11:25:23 +10:00
parent 3af00e085e
commit 8efddc07bc
143 changed files with 831 additions and 593 deletions

View File

@ -106,7 +106,7 @@ function signin_wrapper() {
router.push(route.query.redirect?.toString() ?? "/");
})
.catch((response) => {
const message = response.statusMessage || t("errors.unknown");
const message = response.message || t("errors.unknown");
error.value = message;
})
.finally(() => {

View File

@ -466,7 +466,7 @@ const game = defineModel<ModelType>() as Ref<ModelType>;
if (!game.value)
throw createError({
statusCode: 500,
statusMessage: "Game not provided to editor component",
message: "Game not provided to editor component",
});
const currentTags = ref<{ [key: string]: boolean }>(
@ -553,7 +553,7 @@ function coreMetadataUpdate_wrapper() {
{
title: t("errors.game.metadata.title"),
description: t("errors.game.metadata.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
@ -614,7 +614,7 @@ watch(descriptionHTML, (_v) => {
{
title: t("errors.game.description.title"),
description: t("errors.game.description.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
@ -660,7 +660,7 @@ async function updateBannerImage(id: string) {
{
title: t("errors.game.banner.title"),
description: t("errors.game.banner.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
@ -688,7 +688,7 @@ async function updateCoverImage(id: string) {
{
title: t("errors.game.cover.title"),
description: t("errors.game.cover.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
@ -717,7 +717,7 @@ async function deleteImage(id: string) {
{
title: t("errors.game.deleteImage.title"),
description: t("errors.game.deleteImage.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
@ -761,7 +761,7 @@ async function updateImageCarousel() {
{
title: t("errors.game.carousel.title"),
description: t("errors.game.carousel.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
(e as H3Error)?.message ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},

View File

@ -71,10 +71,6 @@
}}
</div>
<div class="inline-flex items-center gap-x-2">
<component
:is="PLATFORM_ICONS[item.platform]"
class="size-6 text-blue-600"
/>
<Bars3Icon
class="cursor-move w-6 h-6 text-zinc-400 handle"
/>
@ -151,7 +147,7 @@ const game = defineModel<SerializeObject<GameAndVersions>>() as Ref<
if (!game.value)
throw createError({
statusCode: 500,
statusMessage: "Game not provided to editor component",
message: "Game not provided to editor component",
});
async function updateVersionOrder() {
@ -170,7 +166,7 @@ async function updateVersionOrder() {
{
title: t("errors.version.order.title"),
description: t("errors.version.order.desc", {
error: (e as H3Error)?.statusMessage ?? t("errors.unknown"),
error: (e as H3Error)?.message ?? t("errors.unknown"),
}),
buttonText: t("common.close"),
},

View File

@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="flex flex-row gap-x-4">
<div class="relative size-24 bg-zinc-800 rounded-md overflow-hidden">
@ -47,6 +48,83 @@
</div>
</div>
</div>
<SwitchGroup
as="div"
class="max-w-lg flex items-center justify-between gap-x-4"
>
<span class="flex flex-grow flex-col">
<SwitchLabel
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>Create as platform</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400"
>Versions for this redistributable will be able to take a series of
launch commands. Intended to be used with emulators and similar
programs.</SwitchDescription
>
</span>
<Switch
v-model="isPlatform"
:class="[
isPlatform ? 'bg-blue-600' : 'bg-zinc-800',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
]"
>
<span
aria-hidden="true"
:class="[
isPlatform ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
]"
/>
</Switch>
</SwitchGroup>
<div class="relative">
<div class="flex flex-row gap-x-4">
<div class="relative size-24 bg-zinc-800 rounded-md overflow-hidden">
<input
id="platform-icon-upload"
type="file"
class="hidden"
accept="image/svg+xml"
@change="addSvg"
/>
<div
v-if="platform.icon"
class="absolute inset-0 object-cover w-full h-full text-blue-600"
v-html="platform.icon"
/>
<label
for="platform-icon-upload"
class="absolute inset-0 cursor-pointer flex flex-col gap-y-1 items-center justify-center text-zinc-300 bg-zinc-900/50"
>
<ArrowUpTrayIcon class="size-6" />
<span class="text-xs font-bold font-display uppercase"
>Upload SVG</span
>
</label>
</div>
<div class="grow flex flex-col gap-y-4">
<div>
<label
for="platform-name"
class="block text-sm font-medium text-zinc-100"
>Platform Name</label
>
<input
id="platform-name"
v-model="platform.name"
type="text"
class="mt-1 block w-full rounded-md border-0 bg-zinc-950 py-1.5 text-white shadow-sm ring-1 ring-inset ring-zinc-700 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
<div v-if="!isPlatform" class="absolute inset-0 bg-zinc-950/20" />
</div>
<div>
<LoadingButton
class="w-fit"
@ -73,6 +151,12 @@
</template>
<script setup lang="ts">
import {
Switch,
SwitchDescription,
SwitchGroup,
SwitchLabel,
} from "@headlessui/vue";
import { ArrowUpTrayIcon } from "@heroicons/vue/24/outline";
const currentFile = ref<File | undefined>(undefined);
@ -81,11 +165,18 @@ const currentFileObjectUrl = ref<string | undefined>(undefined);
const emit = defineEmits<{
import: [
metadata: { name: string; description: string; icon: File } | undefined,
platform: typeof platform.value | undefined,
];
}>();
const name = ref("");
const description = ref("");
const isPlatform = ref(false);
const platform = ref<{ name: string; icon: string; fileExts: string[] }>({
name: "",
icon: "",
fileExts: [],
});
function addFile(event: Event) {
const file = (event.target as HTMLInputElement)?.files?.[0];
@ -99,6 +190,32 @@ function addFile(event: Event) {
currentFileObjectUrl.value = URL.createObjectURL(file);
}
async function addSvg(event: Event) {
const file = (event.target as HTMLInputElement)?.files?.[0];
if (!file) return;
const svgContent = await file.text();
const parser = new DOMParser();
try {
const document = parser.parseFromString(svgContent, "image/svg+xml");
const svg = document.getElementsByTagName("svg").item(0);
if (!svg) throw "No SVG in uploaded image.";
svg.removeAttribute("width");
svg.removeAttribute("height");
platform.value.icon = svg.outerHTML;
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to upload SVG",
description: (e as string)?.toString() ?? e,
},
(_, c) => c(),
);
return;
}
}
const props = defineProps<{
gameName: string;
loading: boolean;
@ -107,10 +224,14 @@ const props = defineProps<{
function importRedist() {
if (!currentFile.value) return;
emit("import", {
name: name.value,
description: description.value,
icon: currentFile.value,
});
emit(
"import",
{
name: name.value,
description: description.value,
icon: currentFile.value,
},
isPlatform.value ? platform.value : undefined,
);
}
</script>

View File

@ -231,7 +231,7 @@ async function addGame() {
emit("created", game, published.value, developed.value);
} catch (e) {
if (e instanceof FetchError) {
addError.value = e.statusMessage ?? e.message ?? t("errors.unknown");
addError.value = e.message ?? t("errors.unknown");
} else {
throw e;
}

View File

@ -97,13 +97,13 @@ async function createCollection() {
} catch (error) {
console.error("Failed to create collection:", error);
const err = error as { statusMessage?: string };
const err = error as { message?: string };
createModal(
ModalType.Notification,
{
title: t("errors.library.collection.create.title"),
description: t("errors.library.collection.create.desc", [
err?.statusMessage ?? t("errors.unknown"),
err?.message ?? t("errors.unknown"),
]),
},
(_, c) => c(),

View File

@ -67,8 +67,8 @@ async function deleteCollection() {
{
title: t("errors.library.add.title"),
description: t("errors.library.add.desc", [
// @ts-expect-error attempt to display statusMessage on error
e?.statusMessage ?? t("errors.unknown"),
// @ts-expect-error attempt to display message on error
e?.message ?? t("errors.unknown"),
]),
},
(_, c) => c(),

View File

@ -71,8 +71,8 @@ async function deleteArticle() {
{
title: t("errors.news.article.delete.title"),
description: t("errors.news.article.delete.desc", [
// @ts-expect-error attempt to display statusMessage on error
e?.statusMessage ?? t("errors.unknown"),
// @ts-expect-error attempt to display message on error
e?.message ?? t("errors.unknown"),
]),
},
(_, c) => c(),

View File

@ -62,8 +62,8 @@ async function deleteUser() {
{
title: t("errors.admin.user.delete.title"),
description: t("errors.admin.user.delete.desc", [
// @ts-expect-error attempt to display statusMessage on error
e?.statusMessage ?? t("errors.unknown"),
// @ts-expect-error attempt to display message on error
e?.message ?? t("errors.unknown"),
]),
},
(_, c) => c(),

View File

@ -177,7 +177,7 @@ function uploadFile_wrapper() {
uploadLoading.value = true;
uploadFile()
.catch((error) => {
uploadError.value = error.statusMessage ?? t("errors.unknown");
uploadError.value = error.message ?? t("errors.unknown");
})
.finally(() => {
uploadLoading.value = false;

View File

@ -414,8 +414,8 @@ async function createArticle() {
modalOpen.value = false;
} catch (e) {
// @ts-expect-error attempt to get statusMessage on error
error.value = e?.statusMessage ?? t("errors.unknown");
// @ts-expect-error attempt to get message on error
error.value = e?.message ?? t("errors.unknown");
} finally {
loading.value = false;
}

View File

@ -377,6 +377,8 @@ const props = defineProps<{
const tags =
await $dropFetch<Array<SerializeObject<GameTagModel>>>("/api/v1/store/tags");
const userPlatforms = await $dropFetch("/api/v1/store/platforms");
const sorts: Array<StoreSortOption> = [
{
name: "Default",
@ -408,7 +410,10 @@ const options: Array<StoreFilterOption> = [
name: "Platform",
param: "platform",
multiple: true,
options: Object.values(Platform).map((e) => ({ name: e, param: e })),
options: [
...Object.values(Platform).map((e) => ({ name: e, param: e })),
...userPlatforms.map((e) => ({ name: e.platformName, param: e.id })),
],
},
...(props.extraOptions ?? []),
];