mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-24 08:53:04 +10:00
Game specialisation & delta versions (#323)
* feat: game specialisation, auto-guess extensions * fix: enforce specialisation specific schema at API level * fix: lint * feat: partial work on depot endpoints * feat: bump torrential * feat: dummy version creation for depot uploads * fix: lint * fix: types * fix: lint * feat: depot version import * fix: lint * fix: remove any type * fix: lint * fix: push update interval * fix: cpu usage calculation * feat: delta version support * feat: style tweaks for selectlaunch.vue * fix: lint
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4 max-w-[35vw]">
|
||||
<div class="flex flex-col gap-y-4 sm:max-w-[40rem]">
|
||||
<Listbox
|
||||
as="div"
|
||||
:model-value="currentlySelectedVersion"
|
||||
@@ -13,7 +13,7 @@
|
||||
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"
|
||||
>
|
||||
<span v-if="currentlySelectedVersion != -1" class="block truncate">{{
|
||||
versions[currentlySelectedVersion]
|
||||
versions[currentlySelectedVersion].name
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-600">{{
|
||||
$t("library.admin.import.selectDir")
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="(version, versionIdx) in versions"
|
||||
:key="version"
|
||||
:key="version.identifier"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="versionIdx"
|
||||
@@ -54,7 +54,7 @@
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version }}</span
|
||||
>{{ version.name }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
@@ -92,7 +92,7 @@
|
||||
<li
|
||||
v-for="(launch, launchIdx) in versionSettings.setups"
|
||||
:key="launchIdx"
|
||||
class="py-2 inline-flex items-start gap-x-1"
|
||||
class="py-2 inline-flex items-start gap-x-1 w-full"
|
||||
>
|
||||
<ImportVersionLaunchRow
|
||||
v-model="versionSettings.setups[launchIdx]"
|
||||
@@ -122,37 +122,43 @@
|
||||
>
|
||||
</div>
|
||||
<!-- setup mode -->
|
||||
<SwitchGroup
|
||||
as="div"
|
||||
class="bg-zinc-800 p-4 rounded-xl flex items-center justify-between gap-4"
|
||||
>
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>{{ $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"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? 'bg-blue-600' : 'bg-zinc-900',
|
||||
'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',
|
||||
]"
|
||||
<div class="relative">
|
||||
<SwitchGroup
|
||||
as="div"
|
||||
class="bg-zinc-800 p-4 rounded-xl flex items-center justify-between gap-4"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>{{ $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"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? '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',
|
||||
versionSettings.onlySetup ? 'bg-blue-600' : 'bg-zinc-900',
|
||||
'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',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? '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
|
||||
v-if="type === GameType.Redist"
|
||||
class="absolute inset-0 bg-zinc-900/50"
|
||||
/>
|
||||
</div>
|
||||
<!-- launch executables -->
|
||||
<div class="relative flex flex-col gap-y-2 bg-zinc-800 p-4 rounded-xl">
|
||||
<div>
|
||||
@@ -172,19 +178,48 @@
|
||||
:key="launchIdx"
|
||||
class="py-2 inline-flex items-start gap-x-1 w-full"
|
||||
>
|
||||
<ImportVersionLaunchRow
|
||||
v-model="versionSettings.launches[launchIdx]"
|
||||
:version-guesses="versionGuesses"
|
||||
:needs-name="true"
|
||||
/>
|
||||
<button
|
||||
class="transition rounded p-1 bg-zinc-900/30 group hover:bg-red-600/30"
|
||||
@click="() => versionSettings.launches.splice(launchIdx, 1)"
|
||||
<Disclosure
|
||||
v-slot="{ open }"
|
||||
:default-open="true"
|
||||
as="div"
|
||||
class="py-2 px-3 w-full bg-zinc-900 rounded-lg"
|
||||
>
|
||||
<TrashIcon
|
||||
class="transition size-5 text-zinc-700 group-hover:text-red-700"
|
||||
/>
|
||||
</button>
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="flex w-full items-center text-left text-white"
|
||||
>
|
||||
<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 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" />
|
||||
</span>
|
||||
<button
|
||||
class="ml-1 transition rounded p-1 bg-zinc-900/30 group hover:bg-red-600/30"
|
||||
@click.prevent="
|
||||
() => versionSettings.launches.splice(launchIdx, 1)
|
||||
"
|
||||
>
|
||||
<TrashIcon
|
||||
class="transition size-5 text-zinc-700 group-hover:text-red-700"
|
||||
/>
|
||||
</button>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2">
|
||||
<ImportVersionLaunchRow
|
||||
v-model="versionSettings.launches[launchIdx]"
|
||||
:version-guesses="versionGuesses"
|
||||
:needs-name="true"
|
||||
:allow-executor="true"
|
||||
:type="type"
|
||||
/>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</li>
|
||||
</ol>
|
||||
<span
|
||||
@@ -295,15 +330,21 @@ import {
|
||||
SwitchDescription,
|
||||
SwitchGroup,
|
||||
SwitchLabel,
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/vue";
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
TrashIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import type { Platform } from "~/prisma/client/enums";
|
||||
import { GameType } from "~/prisma/client/enums";
|
||||
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
||||
import type { VersionGuess } from "~/server/internal/library";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
@@ -313,20 +354,21 @@ const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const gameId = route.params.id.toString();
|
||||
const versions = await $dropFetch(
|
||||
const { versions, type } = await $dropFetch(
|
||||
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
||||
);
|
||||
const currentlySelectedVersion = ref(-1);
|
||||
const versionSettings = ref<typeof ImportVersion.infer>({
|
||||
id: gameId,
|
||||
version: "",
|
||||
delta: false,
|
||||
onlySetup: false,
|
||||
launches: [],
|
||||
setups: [],
|
||||
});
|
||||
const versionSettings = ref<Omit<typeof ImportVersion.infer, "version" | "id">>(
|
||||
{
|
||||
delta: false,
|
||||
onlySetup: type === GameType.Redist,
|
||||
launches: [],
|
||||
setups: [],
|
||||
requiredContent: [],
|
||||
},
|
||||
);
|
||||
|
||||
const versionGuesses = ref<Array<{ platform: Platform; filename: string }>>();
|
||||
const versionGuesses = ref<Array<VersionGuess>>();
|
||||
|
||||
const importLoading = ref(false);
|
||||
const importError = ref<string | undefined>();
|
||||
@@ -336,14 +378,14 @@ async function updateCurrentlySelectedVersion(value: number) {
|
||||
currentlySelectedVersion.value = value;
|
||||
const version = versions[currentlySelectedVersion.value];
|
||||
try {
|
||||
const results = await $dropFetch(
|
||||
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
||||
gameId,
|
||||
)}&version=${encodeURIComponent(version)}`,
|
||||
{
|
||||
failTitle: "Failed to fetch version information",
|
||||
const results = await $dropFetch(`/api/v1/admin/import/version/preload`, {
|
||||
failTitle: "Failed to fetch version information",
|
||||
query: {
|
||||
id: gameId,
|
||||
type: version.type,
|
||||
version: version.identifier,
|
||||
},
|
||||
);
|
||||
});
|
||||
versionGuesses.value = results as typeof versionGuesses.value;
|
||||
} catch {
|
||||
currentlySelectedVersion.value = -1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-6 w-full max-w-md">
|
||||
<div class="flex flex-col gap-y-6 w-full max-w-lg">
|
||||
<Listbox
|
||||
as="div"
|
||||
:model="currentlySelectedGame"
|
||||
@@ -114,6 +114,40 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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
|
||||
v-model="importMode"
|
||||
type="radio"
|
||||
name="mailing-list"
|
||||
:value="type"
|
||||
:checked="importMode === type"
|
||||
class="absolute inset-0 opacity-0 focus:outline-none"
|
||||
/>
|
||||
<div class="flex flex-col grow">
|
||||
<span class="block text-sm font-medium text-white">{{
|
||||
meta.title
|
||||
}}</span>
|
||||
<span class="mt-1 block text-xs text-gray-400">{{
|
||||
meta.description
|
||||
}}</span>
|
||||
</div>
|
||||
<CheckCircleIcon
|
||||
class="invisible size-5 text-blue-500 group-has-checked:visible"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- without metadata option -->
|
||||
<div>
|
||||
<LoadingButton
|
||||
@@ -309,6 +343,7 @@ import {
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
||||
import type { GameType } from "~/prisma/client/enums";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
|
||||
definePageMeta({
|
||||
@@ -326,6 +361,25 @@ const gameSearchTerm = ref("");
|
||||
const gameSearchLoading = ref(false);
|
||||
const bulkImportMode = ref(false);
|
||||
|
||||
const importModes: {
|
||||
[key in GameType]: { title: string; description: string };
|
||||
} = {
|
||||
Game: {
|
||||
title: "Game",
|
||||
description: "Games are shown in store, and are discoverable.",
|
||||
},
|
||||
Executor: {
|
||||
title: "Executor",
|
||||
description:
|
||||
"Executors are used to launch games. Mainly emulators or wrappers.",
|
||||
},
|
||||
Redist: {
|
||||
title: "Redistributable",
|
||||
description:
|
||||
"Additional content that must be downloaded and installed before running the game.",
|
||||
},
|
||||
};
|
||||
|
||||
async function updateSelectedGame(value: number) {
|
||||
if (currentlySelectedGame.value == value) return;
|
||||
currentlySelectedGame.value = value;
|
||||
@@ -374,6 +428,7 @@ const router = useRouter();
|
||||
|
||||
const importLoading = ref(false);
|
||||
const importError = ref<string | undefined>();
|
||||
const importMode = ref<GameType>("Game");
|
||||
async function importGame(useMetadata: boolean) {
|
||||
if (!metadataResults.value && useMetadata) return;
|
||||
|
||||
@@ -389,6 +444,7 @@ async function importGame(useMetadata: boolean) {
|
||||
path: option.game,
|
||||
library: option.library.id,
|
||||
metadata,
|
||||
type: importMode.value,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<h3
|
||||
class="gap-x-2 text-sm inline-flex items-center font-medium text-zinc-100 font-display"
|
||||
class="gap-2 text-sm flex flex-wrap items-center font-medium text-zinc-100 font-display"
|
||||
>
|
||||
{{ game.mName }}
|
||||
<button
|
||||
@@ -128,6 +128,10 @@
|
||||
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
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-green-600/10 px-2 py-1 text-xs font-medium text-green-600 ring-1 ring-inset ring-green-600/20"
|
||||
>{{ game.type }}</span
|
||||
>
|
||||
</h3>
|
||||
<dl class="mt-1 flex flex-col justify-between">
|
||||
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
|
||||
|
||||
Reference in New Issue
Block a user