mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 08:12:40 +10:00
feat: beginnings of platform & redist management
This commit is contained in:
@ -129,7 +129,7 @@
|
|||||||
<LoadingButton
|
<LoadingButton
|
||||||
class="w-fit"
|
class="w-fit"
|
||||||
:loading="props.loading"
|
:loading="props.loading"
|
||||||
:disabled="!(name && description && currentFileObjectUrl)"
|
:disabled="buttonDisabled"
|
||||||
@click="() => importRedist()"
|
@click="() => importRedist()"
|
||||||
>
|
>
|
||||||
{{ $t("library.admin.import.import") }}
|
{{ $t("library.admin.import.import") }}
|
||||||
@ -178,6 +178,8 @@ const platform = ref<{ name: string; icon: string; fileExts: string[] }>({
|
|||||||
fileExts: [],
|
fileExts: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buttonDisabled = computed<boolean>(() => !(name.value && description.value && currentFileObjectUrl.value && (!isPlatform.value || (platform.value.name && platform.value.icon))))
|
||||||
|
|
||||||
function addFile(event: Event) {
|
function addFile(event: Event) {
|
||||||
const file = (event.target as HTMLInputElement)?.files?.[0];
|
const file = (event.target as HTMLInputElement)?.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updateUser();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|||||||
@ -281,8 +281,8 @@
|
|||||||
"addGames": "All Games",
|
"addGames": "All Games",
|
||||||
"addToLib": "Add to Library",
|
"addToLib": "Add to Library",
|
||||||
"admin": {
|
"admin": {
|
||||||
"detectedGame": "Drop has detected you have new games to import.",
|
"detectedGame": "Drop has detected you have new items to import.",
|
||||||
"detectedVersion": "Drop has detected you have new versions of this game to import.",
|
"detectedVersion": "Drop has detected you have new versions to import.",
|
||||||
"game": {
|
"game": {
|
||||||
"addCarouselNoImages": "No images to add.",
|
"addCarouselNoImages": "No images to add.",
|
||||||
"addDescriptionNoImages": "No images to add.",
|
"addDescriptionNoImages": "No images to add.",
|
||||||
@ -429,7 +429,7 @@
|
|||||||
"title": "Libraries",
|
"title": "Libraries",
|
||||||
"version": {
|
"version": {
|
||||||
"delta": "Upgrade mode",
|
"delta": "Upgrade mode",
|
||||||
"noVersions": "You have no versions of this game available.",
|
"noVersions": "No versions available.",
|
||||||
"noVersionsAdded": "no versions added"
|
"noVersionsAdded": "no versions added"
|
||||||
},
|
},
|
||||||
"versionPriority": "Version priority"
|
"versionPriority": "Version priority"
|
||||||
|
|||||||
@ -37,7 +37,6 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"cookie-es": "^2.0.0",
|
"cookie-es": "^2.0.0",
|
||||||
"dompurify": "^3.2.6",
|
|
||||||
"fast-fuzzy": "^1.12.0",
|
"fast-fuzzy": "^1.12.0",
|
||||||
"file-type-mime": "^0.4.3",
|
"file-type-mime": "^0.4.3",
|
||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
|
|||||||
@ -318,12 +318,15 @@ async function importRedist(data: object, platform: object | undefined) {
|
|||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
formData.append(
|
formData.append(
|
||||||
key,
|
key,
|
||||||
typeof value === "object" ? JSON.stringify(value) : value,
|
value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform) {
|
if (platform) {
|
||||||
for (const [key, value] of Object.entries(platform)) {
|
for (const [key, value] of Object.entries(platform)) {
|
||||||
|
// Because we know there will be no file, and we need to handle more complex objects for
|
||||||
|
// the platform, we do this.
|
||||||
|
// Maybe we shouldn't.
|
||||||
formData.append(
|
formData.append(
|
||||||
`platform.${key}`,
|
`platform.${key}`,
|
||||||
typeof value === "object" ? JSON.stringify(value) : value,
|
typeof value === "object" ? JSON.stringify(value) : value,
|
||||||
|
|||||||
@ -71,41 +71,59 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-x-4 text-zinc-300 font-bold uppercase font-display text-sm">
|
||||||
|
<span class="inline-flex items-center gap-x-1"
|
||||||
|
><div class="size-2 rounded-full bg-blue-600" />
|
||||||
|
Game</span
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center gap-x-1"
|
||||||
|
><div class="size-2 rounded-full bg-emerald-600" />
|
||||||
|
Redistributable</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<ul
|
<ul
|
||||||
role="list"
|
role="list"
|
||||||
class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4"
|
class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="game in filteredLibraryGames"
|
v-for="entry in filteredLibrary"
|
||||||
:key="game.id"
|
:key="entry.id"
|
||||||
class="relative overflow-hidden col-span-1 flex flex-col justify-center divide-y divide-zinc-800 rounded-xl bg-zinc-950/30 text-left shadow-md border hover:scale-102 hover:shadow-xl hover:bg-zinc-950/70 border-zinc-800 transition-all duration-200 group"
|
class="relative overflow-hidden col-span-1 flex flex-col justify-center divide-y divide-zinc-800 rounded-xl bg-zinc-950/30 text-left shadow-md border hover:scale-102 hover:shadow-xl hover:bg-zinc-950/70 border-zinc-800 transition-all duration-200 group"
|
||||||
>
|
>
|
||||||
<div class="flex flex-1 flex-row p-4 gap-x-4">
|
<div
|
||||||
|
v-if="entry.type === 'game'"
|
||||||
|
class="relative flex flex-1 flex-row p-4 gap-x-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 right-0 w-10 bg-blue-600 h-4 rotate-[45deg] translate-x-1/2"
|
||||||
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="h-20 w-20 p-3 flex-shrink-0 rounded-xl shadow group-hover:shadow-lg transition-all duration-200 bg-zinc-900 object-cover border border-zinc-800"
|
class="h-20 w-20 p-3 flex-shrink-0 rounded-xl shadow group-hover:shadow-lg transition-all duration-200 bg-zinc-900 object-cover border border-zinc-800"
|
||||||
:src="useObject(game.mIconObjectId)"
|
:src="useObject(entry.mIconObjectId)"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3
|
<h3
|
||||||
class="gap-x-2 text-sm inline-flex items-center font-medium text-zinc-100 font-display"
|
class="gap-x-2 text-sm inline-flex items-center font-medium text-zinc-100 font-display"
|
||||||
>
|
>
|
||||||
{{ game.mName }}
|
{{ entry.mName }}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
:class="[
|
||||||
'rounded-full p-1 shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2',
|
'rounded-full p-1 shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
game.featured
|
entry.featured
|
||||||
? 'bg-yellow-400 hover:bg-yellow-300 focus-visible:outline-yellow-400 text-zinc-900'
|
? 'bg-yellow-400 hover:bg-yellow-300 focus-visible:outline-yellow-400 text-zinc-900'
|
||||||
: 'bg-zinc-800 hover:bg-zinc-700 focus-visible:outline-zinc-400 text-white',
|
: 'bg-zinc-800 hover:bg-zinc-700 focus-visible:outline-zinc-400 text-white',
|
||||||
]"
|
]"
|
||||||
@click="() => featureGame(game.id)"
|
@click="() => featureGame(entry.id)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="gameFeatureLoading[game.id]"
|
v-if="gameFeatureLoading[entry.id]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
:class="[
|
:class="[
|
||||||
game.featured ? ' fill-zinc-900' : 'fill-zinc-100',
|
entry.featured ? ' fill-zinc-900' : 'fill-zinc-100',
|
||||||
'size-3 text-transparent animate-spin',
|
'size-3 text-transparent animate-spin',
|
||||||
]"
|
]"
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
@ -126,13 +144,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center rounded-full bg-blue-600/10 px-2 py-1 text-xs font-medium text-blue-600 ring-1 ring-inset ring-blue-600/20"
|
class="inline-flex items-center rounded-full bg-blue-600/10 px-2 py-1 text-xs font-medium text-blue-600 ring-1 ring-inset ring-blue-600/20"
|
||||||
>{{ game.library!.name }}</span
|
>{{ entry.library.name }}</span
|
||||||
>
|
>
|
||||||
</h3>
|
</h3>
|
||||||
<dl class="mt-1 flex flex-col justify-between">
|
<dl class="mt-1 flex flex-col justify-between">
|
||||||
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
|
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
|
||||||
<dd class="text-sm text-zinc-400">
|
<dd class="text-sm text-zinc-400">
|
||||||
{{ game.mShortDescription }}
|
{{ entry.mShortDescription }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="sr-only">
|
<dt class="sr-only">
|
||||||
{{ $t("library.admin.metadataProvider") }}
|
{{ $t("library.admin.metadataProvider") }}
|
||||||
@ -140,7 +158,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
<div class="mt-4 flex flex-col gap-y-1">
|
<div class="mt-4 flex flex-col gap-y-1">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:href="`/admin/library/g/${game.id}`"
|
:href="`/admin/library/g/${entry.id}`"
|
||||||
class="w-fit rounded-md bg-zinc-800 px-2.5 py-1.5 text-sm font-semibold 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"
|
class="w-fit rounded-md bg-zinc-800 px-2.5 py-1.5 text-sm font-semibold 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"
|
||||||
>
|
>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@ -155,16 +173,79 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<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"
|
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)"
|
@click="() => deleteGame(entry.id)"
|
||||||
>
|
>
|
||||||
{{ $t("delete") }}
|
{{ $t("delete") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="game.hasNotifications" class="flex flex-col gap-y-2 p-2">
|
<div
|
||||||
|
v-else-if="entry.type === 'redist'"
|
||||||
|
class="relative flex flex-1 flex-row p-4 gap-x-4"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="game.notifications.toImport"
|
class="absolute top-0 right-0 w-10 bg-emerald-600 h-4 rotate-[45deg] translate-x-1/2"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="h-20 w-20 p-3 flex-shrink-0 rounded-xl shadow group-hover:shadow-lg transition-all duration-200 bg-zinc-900 object-cover border border-zinc-800"
|
||||||
|
:src="useObject(entry.mIconObjectId)"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3
|
||||||
|
class="gap-x-2 text-sm inline-flex items-center font-medium text-zinc-100 font-display"
|
||||||
|
>
|
||||||
|
{{ entry.mName }}
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full bg-blue-600/10 px-2 py-1 text-xs font-medium text-blue-600 ring-1 ring-inset ring-blue-600/20"
|
||||||
|
>{{ entry.library.name }}</span
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
<dl class="mt-1 flex flex-col justify-between">
|
||||||
|
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
|
||||||
|
<dd class="text-sm text-zinc-400">
|
||||||
|
{{ entry.mShortDescription }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="entry.platform"
|
||||||
|
class="mt-2 flex items-center text-zinc-200 font-semibold text-sm gap-x-1 p-2 bg-zinc-900 rounded-xl"
|
||||||
|
>
|
||||||
|
<IconsPlatform
|
||||||
|
:platform="entry.platform.id"
|
||||||
|
:fallback="entry.platform.iconSvg"
|
||||||
|
class="size-6 text-blue-600"
|
||||||
|
/>
|
||||||
|
<span>{{ entry.platform.platformName }}</span>
|
||||||
|
</dl>
|
||||||
|
<div class="mt-4 flex flex-col gap-y-1">
|
||||||
|
<NuxtLink
|
||||||
|
:href="`/admin/library/r/${entry.id}`"
|
||||||
|
class="w-fit rounded-md bg-zinc-800 px-2.5 py-1.5 text-sm font-semibold 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"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="library.admin.openEditor"
|
||||||
|
tag="span"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<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="() => deleteRedist(entry.id)"
|
||||||
|
>
|
||||||
|
{{ $t("delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="entry.hasNotifications" class="flex flex-col gap-y-2 p-2">
|
||||||
|
<div
|
||||||
|
v-if="entry.notifications.toImport"
|
||||||
class="rounded-md bg-blue-600/10 p-4"
|
class="rounded-md bg-blue-600/10 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@ -180,7 +261,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:href="`/admin/library/g/${game.id}/import`"
|
:href="`/admin/library/g/${entry.id}/import`"
|
||||||
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
||||||
>
|
>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@ -198,7 +279,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="game.notifications.noVersions"
|
v-if="entry.notifications.noVersions"
|
||||||
class="rounded-md bg-yellow-600/10 p-4"
|
class="rounded-md bg-yellow-600/10 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@ -216,7 +297,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="game.notifications.offline"
|
v-if="entry.notifications.offline"
|
||||||
class="rounded-md bg-red-600/10 p-4"
|
class="rounded-md bg-red-600/10 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@ -236,14 +317,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<p
|
<p
|
||||||
v-if="filteredLibraryGames.length == 0 && libraryGames.length != 0"
|
v-if="filteredLibrary.length == 0 && libraryGames.length != 0"
|
||||||
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
|
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
|
||||||
>
|
>
|
||||||
{{ $t("common.noResults") }}
|
{{ $t("common.noResults") }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="
|
||||||
filteredLibraryGames.length == 0 &&
|
filteredLibrary.length == 0 &&
|
||||||
libraryGames.length == 0 &&
|
libraryGames.length == 0 &&
|
||||||
libraryState.hasLibraries
|
libraryState.hasLibraries
|
||||||
"
|
"
|
||||||
@ -305,29 +386,33 @@ useHead({
|
|||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
|
|
||||||
const libraryState = await $dropFetch("/api/v1/admin/library");
|
const libraryState = await $dropFetch("/api/v1/admin/library");
|
||||||
type LibraryStateGame = (typeof libraryState.games)[number]["game"];
|
|
||||||
|
|
||||||
const toImport = ref(
|
const toImport = ref(
|
||||||
Object.values(libraryState.unimportedGames).flat().length > 0,
|
Object.values(libraryState.unimportedGames).flat().length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const libraryGames = ref<
|
// Potentially make a server-side transformation to make the client lighter
|
||||||
Array<
|
function clientSideTransformation<T, V extends keyof T, K extends string>(
|
||||||
LibraryStateGame & {
|
values: Array<T & { status: (typeof libraryState.games)[number]["status"] }>,
|
||||||
status: "online" | "offline";
|
expand: V,
|
||||||
hasNotifications?: boolean;
|
type: K,
|
||||||
notifications: {
|
): Array<
|
||||||
noVersions?: boolean;
|
T[V] & {
|
||||||
toImport?: boolean;
|
status: "online" | "offline";
|
||||||
offline?: boolean;
|
type: K;
|
||||||
};
|
hasNotifications?: boolean;
|
||||||
}
|
notifications: {
|
||||||
>
|
noVersions?: boolean;
|
||||||
>(
|
toImport?: boolean;
|
||||||
libraryState.games.map((e) => {
|
offline?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return values.map((e) => {
|
||||||
if (e.status == "offline") {
|
if (e.status == "offline") {
|
||||||
return {
|
return {
|
||||||
...e.game,
|
...e[expand],
|
||||||
|
type: type,
|
||||||
status: "offline" as const,
|
status: "offline" as const,
|
||||||
hasNotifications: true,
|
hasNotifications: true,
|
||||||
notifications: {
|
notifications: {
|
||||||
@ -340,7 +425,8 @@ const libraryGames = ref<
|
|||||||
const toImport = e.status.unimportedVersions.length > 0;
|
const toImport = e.status.unimportedVersions.length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...e.game,
|
...e[expand],
|
||||||
|
type: type,
|
||||||
notifications: {
|
notifications: {
|
||||||
noVersions,
|
noVersions,
|
||||||
toImport,
|
toImport,
|
||||||
@ -348,13 +434,18 @@ const libraryGames = ref<
|
|||||||
hasNotifications: noVersions || toImport,
|
hasNotifications: noVersions || toImport,
|
||||||
status: "online" as const,
|
status: "online" as const,
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryGames = ref(
|
||||||
|
clientSideTransformation(libraryState.games, "value", "game"),
|
||||||
|
);
|
||||||
|
const libraryRedists = ref(
|
||||||
|
clientSideTransformation(libraryState.redists, "value", "redist"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredLibraryGames = computed(() =>
|
const filteredLibrary = computed(() =>
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
[...libraryGames.value, ...libraryRedists.value].filter((e) => {
|
||||||
// @ts-ignore excessively deep ts
|
|
||||||
libraryGames.value.filter((e) => {
|
|
||||||
if (!searchQuery.value) return true;
|
if (!searchQuery.value) return true;
|
||||||
const searchQueryLower = searchQuery.value.toLowerCase();
|
const searchQueryLower = searchQuery.value.toLowerCase();
|
||||||
if (e.mName.toLowerCase().includes(searchQueryLower)) return true;
|
if (e.mName.toLowerCase().includes(searchQueryLower)) return true;
|
||||||
@ -374,6 +465,16 @@ async function deleteGame(id: string) {
|
|||||||
toImport.value = true;
|
toImport.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteRedist(id: string) {
|
||||||
|
await $dropFetch(`/api/v1/admin/redist/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
failTitle: "Failed to delete game",
|
||||||
|
});
|
||||||
|
const index = libraryRedists.value.findIndex((e) => e.id === id);
|
||||||
|
libraryRedists.value.splice(index, 1);
|
||||||
|
toImport.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
const gameFeatureLoading = ref<{ [key: string]: boolean }>({});
|
const gameFeatureLoading = ref<{ [key: string]: boolean }>({});
|
||||||
async function featureGame(id: string) {
|
async function featureGame(id: string) {
|
||||||
const gameIndex = libraryGames.value.findIndex((e) => e.id === id);
|
const gameIndex = libraryGames.value.findIndex((e) => e.id === id);
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `libraryId` on table `Game` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "public"."GameTag_name_idx";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."Game" ALTER COLUMN "libraryId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
|
||||||
@ -34,10 +34,8 @@ model Game {
|
|||||||
versions Version[]
|
versions Version[]
|
||||||
mods Mod[]
|
mods Mod[]
|
||||||
|
|
||||||
// These fields will not be optional in the next version
|
libraryId String
|
||||||
// Any game without a library ID will be assigned one at startup, based on the defaults
|
library Library @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
libraryId String?
|
|
||||||
library Library? @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
|
||||||
libraryPath String
|
libraryPath String
|
||||||
|
|
||||||
collections CollectionEntry[]
|
collections CollectionEntry[]
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import * as jdenticon from "jdenticon";
|
|||||||
import prisma from "~/server/internal/db/database";
|
import prisma from "~/server/internal/db/database";
|
||||||
import libraryManager from "~/server/internal/library";
|
import libraryManager from "~/server/internal/library";
|
||||||
import jsdom from "jsdom";
|
import jsdom from "jsdom";
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
export const ImportRedist = type({
|
export const ImportRedist = type({
|
||||||
library: "string",
|
library: "string",
|
||||||
@ -28,7 +27,8 @@ export default defineEventHandler(async (h3) => {
|
|||||||
const body = await handleFileUpload(h3, {}, ["internal:read"], 1);
|
const body = await handleFileUpload(h3, {}, ["internal:read"], 1);
|
||||||
if (!body) throw createError({ statusCode: 400, message: "Body required." });
|
if (!body) throw createError({ statusCode: 400, message: "Body required." });
|
||||||
|
|
||||||
const [[id], rawOptions, pull, , add] = body;
|
const [ids, rawOptions, pull, , add] = body;
|
||||||
|
const id = ids.at(0);
|
||||||
|
|
||||||
const options = ImportRedist(rawOptions);
|
const options = ImportRedist(rawOptions);
|
||||||
if (options instanceof ArkErrors)
|
if (options instanceof ArkErrors)
|
||||||
@ -60,7 +60,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
});
|
});
|
||||||
svg.removeAttribute("width");
|
svg.removeAttribute("width");
|
||||||
svg.removeAttribute("height");
|
svg.removeAttribute("height");
|
||||||
svgContent = DOMPurify.sanitize(svg.outerHTML, {USE_PROFILES: {svg: true, svgFilters: true}});
|
svgContent = svg.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redist = await prisma.redist.create({
|
const redist = await prisma.redist.create({
|
||||||
|
|||||||
@ -7,9 +7,15 @@ export default defineEventHandler(async (h3) => {
|
|||||||
|
|
||||||
const unimportedGames = await libraryManager.fetchUnimportedGames();
|
const unimportedGames = await libraryManager.fetchUnimportedGames();
|
||||||
const games = await libraryManager.fetchGamesWithStatus();
|
const games = await libraryManager.fetchGamesWithStatus();
|
||||||
|
const redists = await libraryManager.fetchRedistsWithStatus();
|
||||||
const libraries = await libraryManager.fetchLibraries();
|
const libraries = await libraryManager.fetchLibraries();
|
||||||
|
|
||||||
// Fetch other library data here
|
// Fetch other library data here
|
||||||
|
|
||||||
return { unimportedGames, games, hasLibraries: libraries.length > 0 };
|
return {
|
||||||
|
unimportedGames,
|
||||||
|
games,
|
||||||
|
redists,
|
||||||
|
hasLibraries: libraries.length > 0,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
19
server/api/v1/admin/redist/[id]/index.delete.ts
Normal file
19
server/api/v1/admin/redist/[id]/index.delete.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["redist:delete"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const id = getRouterParam(h3, "id")!;
|
||||||
|
|
||||||
|
const { count } = await prisma.redist.deleteMany({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count == 0) throw createError({ statusCode: 404 });
|
||||||
|
|
||||||
|
return;
|
||||||
|
});
|
||||||
16
server/api/v1/admin/redist/index.get.ts
Normal file
16
server/api/v1/admin/redist/index.get.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["redist:read"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
return await prisma.redist.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
mName: true,
|
||||||
|
mShortDescription: true,
|
||||||
|
mIconObjectId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -73,6 +73,10 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
|||||||
"game:image:new": "Upload an image for a game.",
|
"game:image:new": "Upload an image for a game.",
|
||||||
"game:image:delete": "Delete an image for a game.",
|
"game:image:delete": "Delete an image for a game.",
|
||||||
|
|
||||||
|
"redist:read": "Fetch redistributables on this instance.",
|
||||||
|
"redist:update": "Update redistributables on this instance.",
|
||||||
|
"redist:delete": "Delete redistributables on this instance.",
|
||||||
|
|
||||||
"company:read": "Fetch companies.",
|
"company:read": "Fetch companies.",
|
||||||
"company:create": "Create a new company.",
|
"company:create": "Create a new company.",
|
||||||
"company:update": "Update existing companies.",
|
"company:update": "Update existing companies.",
|
||||||
|
|||||||
@ -67,6 +67,10 @@ export const systemACLs = [
|
|||||||
"game:image:new",
|
"game:image:new",
|
||||||
"game:image:delete",
|
"game:image:delete",
|
||||||
|
|
||||||
|
"redist:read",
|
||||||
|
"redist:update",
|
||||||
|
"redist:delete",
|
||||||
|
|
||||||
"company:read",
|
"company:read",
|
||||||
"company:update",
|
"company:update",
|
||||||
"company:create",
|
"company:create",
|
||||||
|
|||||||
@ -128,29 +128,23 @@ class LibraryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchGamesWithStatus() {
|
async fetchLibraryObjectWithStatus<T>(
|
||||||
const games = await prisma.game.findMany({
|
objects: Array<
|
||||||
include: {
|
{
|
||||||
versions: {
|
libraryId: string;
|
||||||
select: {
|
libraryPath: string;
|
||||||
versionName: true,
|
versions: Array<unknown>;
|
||||||
},
|
} & T
|
||||||
},
|
>,
|
||||||
library: true,
|
) {
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
mName: "asc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
games.map(async (e) => {
|
objects.map(async (e) => {
|
||||||
const versions = await this.fetchUnimportedGameVersions(
|
const versions = await this.fetchUnimportedGameVersions(
|
||||||
e.libraryId ?? "",
|
e.libraryId ?? "",
|
||||||
e.libraryPath,
|
e.libraryPath,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
game: e,
|
value: e,
|
||||||
status: versions
|
status: versions
|
||||||
? {
|
? {
|
||||||
noVersions: e.versions.length == 0,
|
noVersions: e.versions.length == 0,
|
||||||
@ -162,6 +156,55 @@ class LibraryManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchGamesWithStatus() {
|
||||||
|
const games = await prisma.game.findMany({
|
||||||
|
include: {
|
||||||
|
versions: {
|
||||||
|
select: {
|
||||||
|
versionId: true,
|
||||||
|
versionName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
library: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
mName: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.fetchLibraryObjectWithStatus(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchRedistsWithStatus() {
|
||||||
|
const redists = await prisma.redist.findMany({
|
||||||
|
include: {
|
||||||
|
versions: {
|
||||||
|
select: {
|
||||||
|
versionId: true,
|
||||||
|
versionName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
library: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
platform: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
mName: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.fetchLibraryObjectWithStatus(redists);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches recommendations and extra data about the version. Doesn't actually check if it's been imported.
|
* Fetches recommendations and extra data about the version. Doesn't actually check if it's been imported.
|
||||||
* @param gameId
|
* @param gameId
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { LibraryBackend } from "~/prisma/client/enums";
|
import type { LibraryBackend } from "~/prisma/client/enums";
|
||||||
import prisma from "../internal/db/database";
|
import prisma from "../internal/db/database";
|
||||||
import type { JsonValue } from "@prisma/client/runtime/library";
|
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||||
import type { LibraryProvider } from "../internal/library/provider";
|
import type { LibraryProvider } from "../internal/library/provider";
|
||||||
import type { FilesystemProviderConfig } from "../internal/library/providers/filesystem";
|
|
||||||
import { FilesystemProvider } from "../internal/library/providers/filesystem";
|
import { FilesystemProvider } from "../internal/library/providers/filesystem";
|
||||||
import libraryManager from "../internal/library";
|
import libraryManager from "../internal/library";
|
||||||
import path from "path";
|
|
||||||
import { FlatFilesystemProvider } from "../internal/library/providers/flat";
|
import { FlatFilesystemProvider } from "../internal/library/providers/flat";
|
||||||
import { logger } from "~/server/internal/logging";
|
import { logger } from "~/server/internal/logging";
|
||||||
|
|
||||||
@ -33,42 +31,6 @@ export default defineNitroPlugin(async () => {
|
|||||||
let successes = 0;
|
let successes = 0;
|
||||||
const libraries = await prisma.library.findMany({});
|
const libraries = await prisma.library.findMany({});
|
||||||
|
|
||||||
// Add migration handler
|
|
||||||
const legacyPath = process.env.LIBRARY;
|
|
||||||
if (legacyPath && libraries.length == 0) {
|
|
||||||
const options: typeof FilesystemProviderConfig.infer = {
|
|
||||||
baseDir: path.resolve(legacyPath),
|
|
||||||
};
|
|
||||||
|
|
||||||
const library = await prisma.library.create({
|
|
||||||
data: {
|
|
||||||
name: "Auto-created",
|
|
||||||
backend: LibraryBackend.Filesystem,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
libraries.push(library);
|
|
||||||
|
|
||||||
// Update all existing games
|
|
||||||
await prisma.game.updateMany({
|
|
||||||
where: {
|
|
||||||
libraryId: null,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
libraryId: library.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all games that don't have a library provider after the legacy handler
|
|
||||||
// (leftover from a bug)
|
|
||||||
await prisma.game.deleteMany({
|
|
||||||
where: {
|
|
||||||
libraryId: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const library of libraries) {
|
for (const library of libraries) {
|
||||||
const constructor = libraryConstructors[library.backend];
|
const constructor = libraryConstructors[library.backend];
|
||||||
try {
|
try {
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@ -2497,11 +2497,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
||||||
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
||||||
|
|
||||||
"@types/trusted-types@^2.0.7":
|
|
||||||
version "2.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
|
||||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
|
||||||
|
|
||||||
"@types/turndown@^5.0.5":
|
"@types/turndown@^5.0.5":
|
||||||
version "5.0.5"
|
version "5.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f"
|
resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f"
|
||||||
@ -4272,13 +4267,6 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
domelementtype "^2.3.0"
|
domelementtype "^2.3.0"
|
||||||
|
|
||||||
dompurify@^3.2.6:
|
|
||||||
version "3.2.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
|
|
||||||
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
|
|
||||||
optionalDependencies:
|
|
||||||
"@types/trusted-types" "^2.0.7"
|
|
||||||
|
|
||||||
domutils@^3.0.1, domutils@^3.2.1, domutils@^3.2.2:
|
domutils@^3.0.1, domutils@^3.2.1, domutils@^3.2.2:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
|
||||||
|
|||||||
Reference in New Issue
Block a user