mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 12:32:09 +10:00
feat: uninstall commands, new R UI
This commit is contained in:
@ -1,96 +1,190 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="game && unimportedVersions">
|
<div v-if="game && unimportedVersions" class="p-8">
|
||||||
<div class="grow flex flex-row gap-y-8">
|
<div>
|
||||||
<div class="grow w-full h-full px-6 py-4 flex flex-col"></div>
|
<div class="sm:flex sm:items-center">
|
||||||
<div
|
<div class="sm:flex-auto">
|
||||||
class="lg:overflow-y-auto lg:border-l lg:border-zinc-800 lg:block lg:inset-y-0 lg:z-50 lg:w-[30vw] flex flex-col gap-y-8 px-6 py-4"
|
<h1 class="text-base font-semibold text-zinc-100">Versions</h1>
|
||||||
>
|
<p class="mt-2 text-sm text-zinc-400 max-w-lg">
|
||||||
<!-- version manager -->
|
Versions are a collection of files that are downloaded to clients.
|
||||||
|
Each version can have multiple configurations, for different
|
||||||
|
platforms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||||
|
<NuxtLink
|
||||||
|
:href="canImport ? `/admin/library/g/${game.id}/import` : ''"
|
||||||
|
type="button"
|
||||||
|
:class="[
|
||||||
|
canImport ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-800/50',
|
||||||
|
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
canImport
|
||||||
|
? $t("library.admin.import.version.import")
|
||||||
|
: $t("library.admin.import.version.noVersions")
|
||||||
|
}}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 rounded-xl border border-zinc-800 bg-zinc-900 shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<!-- version priority -->
|
<table class="min-w-full divide-y divide-zinc-800">
|
||||||
<div>
|
<thead>
|
||||||
<div class="border-b border-zinc-800 pb-3">
|
<tr class="bg-zinc-800/50">
|
||||||
<div
|
<th
|
||||||
class="flex flex-wrap items-center justify-between sm:flex-nowrap"
|
scope="col"
|
||||||
>
|
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-6"
|
||||||
<h3
|
|
||||||
class="text-base font-semibold font-display leading-6 text-zinc-100"
|
|
||||||
>
|
>
|
||||||
{{ $t("library.admin.versionPriority") }}
|
Version Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||||
|
>
|
||||||
|
Imported
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||||
|
>
|
||||||
|
Platforms
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||||
|
<span class="sr-only">{{ $t("actions") }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-zinc-800">
|
||||||
|
<tr
|
||||||
|
v-for="version in game.versions"
|
||||||
|
:key="version.versionId"
|
||||||
|
class="transition-colors duration-150 hover:bg-zinc-800/50"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-6"
|
||||||
|
>
|
||||||
|
{{ version.versionName }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||||
|
<RelativeTime :date="version.created" />
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<li
|
||||||
|
v-for="gameVersion in version.gameVersions"
|
||||||
|
:key="gameVersion.versionId"
|
||||||
|
class="px-3 py-2 bg-zinc-800 rounded-lg shadow"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="text-sm flex items-center text-zinc-200 font-semibold"
|
||||||
|
>
|
||||||
|
<IconsPlatform
|
||||||
|
:platform="
|
||||||
|
platforms[gameVersion.platformId].platformIcon.key
|
||||||
|
"
|
||||||
|
:fallback="
|
||||||
|
platforms[gameVersion.platformId].platformIcon
|
||||||
|
.fallback
|
||||||
|
"
|
||||||
|
class="size-5 text-blue-500"
|
||||||
|
/>
|
||||||
|
<span class="ml-3 block truncate">{{
|
||||||
|
platforms[gameVersion.platformId].name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- import games button -->
|
<!-- launch commands -->
|
||||||
|
<div class="space-y-1 mt-4">
|
||||||
|
<div
|
||||||
|
v-if="gameVersion.install"
|
||||||
|
class="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>Install</span
|
||||||
|
>
|
||||||
|
|
||||||
<NuxtLink
|
<div
|
||||||
:href="canImport ? `/admin/library/g/${game.id}/import` : ''"
|
class="whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
type="button"
|
>
|
||||||
:class="[
|
<span class="text-zinc-700">(install dir)/</span
|
||||||
canImport
|
>{{ gameVersion.install.command }}
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
{{ gameVersion.install.args }}
|
||||||
: 'bg-blue-800/50',
|
</div>
|
||||||
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
|
</div>
|
||||||
]"
|
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-sm text-zinc-100"
|
||||||
|
>Launch options</span
|
||||||
|
>
|
||||||
|
<ul class="divide-y divide-zinc-700">
|
||||||
|
<li
|
||||||
|
v-for="launch in gameVersion.launches"
|
||||||
|
:key="launch.command"
|
||||||
|
class="ml-2 py-2 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
{{ launch.name }}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
class="mt-1 whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-700"
|
||||||
|
>(install dir)/</span
|
||||||
|
>{{ launch.command }} {{ launch.args }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="gameVersion.uninstall"
|
||||||
|
class="flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-display text-xs text-zinc-300 font-semibold uppercase tracking-wide"
|
||||||
|
>Uninstall</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="whitespace-nowrap font-mono text-xs text-zinc-300 bg-zinc-950 px-1 py-0.5 w-fit rounded"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-700">(install dir)/</span
|
||||||
|
>{{ gameVersion.uninstall.command }}
|
||||||
|
{{ gameVersion.uninstall.args }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400 ring-1 ring-inset ring-red-400/20 transition-all duration-200 hover:bg-red-400/20 hover:scale-105 active:scale-95"
|
||||||
|
@click="() => deleteVersion(version.versionId)"
|
||||||
>
|
>
|
||||||
{{
|
Delete
|
||||||
canImport
|
<span class="sr-only">
|
||||||
? $t("library.admin.import.version.import")
|
{{ $t("chars.srComma", [version.versionName]) }}
|
||||||
: $t("library.admin.import.version.noVersions")
|
</span>
|
||||||
}}
|
</button>
|
||||||
</NuxtLink>
|
</td>
|
||||||
</h3>
|
</tr>
|
||||||
</div>
|
<tr v-if="game.versions.length === 0">
|
||||||
</div>
|
<td colspan="5" class="py-8 text-center text-sm text-zinc-400">
|
||||||
|
No versions
|
||||||
<div class="mt-4 text-center w-full text-sm text-zinc-600">
|
</td>
|
||||||
{{ $t("lowest") }}
|
</tr>
|
||||||
</div>
|
</tbody>
|
||||||
<draggable
|
</table>
|
||||||
:list="game.versions"
|
|
||||||
handle=".handle"
|
|
||||||
class="mt-2 space-y-4"
|
|
||||||
@update="() => updateVersionOrder()"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#item="{
|
|
||||||
element: item,
|
|
||||||
}: {
|
|
||||||
element: VersionModel & { gameVersion: GameVersionModel };
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full inline-flex items-center px-4 py-2 bg-zinc-800 rounded justify-between"
|
|
||||||
>
|
|
||||||
<div class="text-zinc-100 font-semibold">
|
|
||||||
{{ item.versionName }}
|
|
||||||
</div>
|
|
||||||
<div class="text-zinc-400">
|
|
||||||
{{
|
|
||||||
item.gameVersion.delta
|
|
||||||
? $t("library.admin.version.delta")
|
|
||||||
: ""
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex items-center gap-x-2">
|
|
||||||
<Bars3Icon
|
|
||||||
class="cursor-move w-6 h-6 text-zinc-400 handle"
|
|
||||||
/>
|
|
||||||
<button @click="() => deleteVersion(item.versionId)">
|
|
||||||
<TrashIcon class="w-5 h-5 text-red-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
<div
|
|
||||||
v-if="game.versions.length == 0"
|
|
||||||
class="text-center font-bold text-zinc-400 my-3"
|
|
||||||
>
|
|
||||||
{{ $t("library.admin.version.noVersionsAdded") }}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-center w-full text-sm text-zinc-600">
|
|
||||||
{{ $t("highest") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,13 +210,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {
|
import type { SerializeObject, TypedInternalResponse } from "nitropack";
|
||||||
GameModel,
|
|
||||||
GameVersionModel,
|
|
||||||
VersionModel,
|
|
||||||
} from "~/prisma/client/models";
|
|
||||||
import { Bars3Icon, TrashIcon } from "@heroicons/vue/24/solid";
|
|
||||||
import type { SerializeObject } from "nitropack";
|
|
||||||
import type { H3Error } from "h3";
|
import type { H3Error } from "h3";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
|
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
|
||||||
@ -138,18 +226,23 @@ const canImport = computed(
|
|||||||
() => hasDeleted.value || props.unimportedVersions.length > 0,
|
() => hasDeleted.value || props.unimportedVersions.length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
type GameAndVersions = GameModel & {
|
type GameFetchType = TypedInternalResponse<
|
||||||
versions: (VersionModel & { gameVersion: GameVersionModel })[];
|
"/api/v1/admin/game/:id",
|
||||||
};
|
unknown,
|
||||||
const game = defineModel<SerializeObject<GameAndVersions>>() as Ref<
|
"get"
|
||||||
SerializeObject<GameAndVersions>
|
>["game"];
|
||||||
>;
|
const game = defineModel<SerializeObject<GameFetchType>>({ required: true });
|
||||||
if (!game.value)
|
if (!game.value)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: "Game not provided to editor component",
|
message: "Game not provided to editor component",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rawPlatforms = await useAdminPlatforms();
|
||||||
|
const platforms = Object.fromEntries(
|
||||||
|
renderPlatforms(rawPlatforms).map((v) => [v.param, v]),
|
||||||
|
);
|
||||||
|
|
||||||
async function updateVersionOrder() {
|
async function updateVersionOrder() {
|
||||||
try {
|
try {
|
||||||
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
|
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
||||||
><slot
|
><slot
|
||||||
/></ListboxLabel>
|
/></ListboxLabel>
|
||||||
<div class="relative mt-2">
|
<div class="relative">
|
||||||
<ListboxButton
|
<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-500 sm:text-sm sm:leading-6"
|
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-500 sm:text-sm sm:leading-6"
|
||||||
>
|
>
|
||||||
|
|||||||
120
components/PreloadSelector.vue
Normal file
120
components/PreloadSelector.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<Combobox
|
||||||
|
as="div"
|
||||||
|
:value="props.value"
|
||||||
|
nullable
|
||||||
|
@update:model-value="(v) => emit('update', v)"
|
||||||
|
>
|
||||||
|
<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="file.exe"
|
||||||
|
@change="query = $event.target.value"
|
||||||
|
@blur="query = ''"
|
||||||
|
/>
|
||||||
|
<ComboboxButton
|
||||||
|
v-if="filtered?.length ?? 0 > 0"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
<ChevronUpDownIcon class="size-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
|
||||||
|
<ComboboxOptions
|
||||||
|
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ComboboxOption
|
||||||
|
v-for="guess in filtered"
|
||||||
|
:key="guess.filename"
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
:value="guess.filename"
|
||||||
|
as="template"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
|
active ? 'bg-blue-600 text-white outline-none' : 'text-zinc-100',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center gap-x-2 block truncate',
|
||||||
|
selected && 'font-semibold',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ guess.filename }}
|
||||||
|
<IconsPlatform
|
||||||
|
:platform="guess.platform.platformIcon.key"
|
||||||
|
:fallback="guess.platform.platformIcon.fallback"
|
||||||
|
class="size-5 flex-shrink-0 text-blue-600"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
:class="[
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||||
|
active ? 'text-white' : 'text-blue-600',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<CheckIcon class="size-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<ComboboxOption
|
||||||
|
v-if="query"
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
:value="query"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
|
active ? 'bg-blue-600 text-white outline-none' : 'text-zinc-100',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span :class="['block truncate', selected && 'font-semibold']">
|
||||||
|
{{ $t("chars.quoted", { text: query }) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
:class="[
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||||
|
active ? 'text-white' : 'text-blue-600',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<CheckIcon class="size-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxOption,
|
||||||
|
ComboboxOptions,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/24/outline";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: [v: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value?: string;
|
||||||
|
guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const query = ref("");
|
||||||
|
|
||||||
|
const filtered = computed(() =>
|
||||||
|
props.guesses?.filter((e) =>
|
||||||
|
e.filename.toLowerCase().includes(query.value.toLowerCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { UserPlatform } from "~/prisma/client/client";
|
||||||
import { HardwarePlatform } from "~/prisma/client/enums";
|
import { HardwarePlatform } from "~/prisma/client/enums";
|
||||||
|
|
||||||
export type PlatformRenderable = {
|
export type PlatformRenderable = {
|
||||||
@ -22,3 +23,14 @@ export function renderPlatforms(
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawUseAdminPlatforms = () => useState<Array<UserPlatform> | null>('adminPlatforms', () => null);
|
||||||
|
|
||||||
|
export async function useAdminPlatforms() {
|
||||||
|
const platforms = rawUseAdminPlatforms();
|
||||||
|
if(platforms.value === null){
|
||||||
|
platforms.value = await $dropFetch("/api/v1/admin/platforms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return platforms.value!
|
||||||
|
}
|
||||||
|
|||||||
@ -159,7 +159,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
typeCheck: true,
|
//typeCheck: true,
|
||||||
|
|
||||||
tsConfig: {
|
tsConfig: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "drop",
|
"name": "drop",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
|||||||
@ -98,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- setup command -->
|
<!-- install command -->
|
||||||
<div class="max-w-lg">
|
<div class="max-w-lg">
|
||||||
<label
|
<label
|
||||||
for="startup"
|
for="startup"
|
||||||
@ -117,110 +117,14 @@
|
|||||||
>
|
>
|
||||||
{{ $t("library.admin.import.version.installDir") }}
|
{{ $t("library.admin.import.version.installDir") }}
|
||||||
</span>
|
</span>
|
||||||
<Combobox
|
<PreloadSelector
|
||||||
as="div"
|
:value="versionSettings.install"
|
||||||
:value="versionSettings.setup"
|
:guesses="versionGuesses"
|
||||||
nullable
|
@update="(v) => updateInstallCommand(v)"
|
||||||
@update:model-value="(v) => updateSetupCommand(v)"
|
/>
|
||||||
>
|
|
||||||
<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="
|
|
||||||
$t('library.admin.import.version.setupPlaceholder')
|
|
||||||
"
|
|
||||||
@change="setupProcessQuery = $event.target.value"
|
|
||||||
@blur="setupProcessQuery = ''"
|
|
||||||
/>
|
|
||||||
<ComboboxButton
|
|
||||||
v-if="setupFilteredVersionGuesses?.length ?? 0 > 0"
|
|
||||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
|
||||||
>
|
|
||||||
<ChevronUpDownIcon
|
|
||||||
class="size-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</ComboboxButton>
|
|
||||||
|
|
||||||
<ComboboxOptions
|
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
|
||||||
>
|
|
||||||
<ComboboxOption
|
|
||||||
v-for="guess in setupFilteredVersionGuesses"
|
|
||||||
:key="guess.filename"
|
|
||||||
v-slot="{ active, selected }"
|
|
||||||
:value="guess.filename"
|
|
||||||
as="template"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
:class="[
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
|
||||||
active
|
|
||||||
? 'bg-blue-600 text-white outline-none'
|
|
||||||
: 'text-zinc-100',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center gap-x-2 block truncate',
|
|
||||||
selected && 'font-semibold',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ guess.filename }}
|
|
||||||
<IconsPlatform
|
|
||||||
:platform="guess.platform.platformIcon.key"
|
|
||||||
:fallback="guess.platform.platformIcon.fallback"
|
|
||||||
class="size-5 flex-shrink-0 text-blue-600"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
:class="[
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
<ComboboxOption
|
|
||||||
v-if="setupProcessQuery"
|
|
||||||
v-slot="{ active, selected }"
|
|
||||||
:value="setupProcessQuery"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
:class="[
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
|
||||||
active
|
|
||||||
? 'bg-blue-600 text-white outline-none'
|
|
||||||
: 'text-zinc-100',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="['block truncate', selected && 'font-semibold']"
|
|
||||||
>
|
|
||||||
{{ $t("chars.quoted", { text: setupProcessQuery }) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
:class="[
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
</ComboboxOptions>
|
|
||||||
</div>
|
|
||||||
</Combobox>
|
|
||||||
<input
|
<input
|
||||||
id="startup"
|
id="startup"
|
||||||
v-model="versionSettings.setupArgs"
|
v-model="versionSettings.installArgs"
|
||||||
type="text"
|
type="text"
|
||||||
name="startup"
|
name="startup"
|
||||||
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||||
@ -290,110 +194,11 @@
|
|||||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||||
>{{ $t("library.admin.import.version.installDir") }}</span
|
>{{ $t("library.admin.import.version.installDir") }}</span
|
||||||
>
|
>
|
||||||
<Combobox
|
<PreloadSelector
|
||||||
as="div"
|
|
||||||
:value="launch.launchCommand"
|
:value="launch.launchCommand"
|
||||||
nullable
|
:guesses="versionGuesses"
|
||||||
@update:model-value="(v) => updateLaunchCommand(launchIdx, v)"
|
@update="(v) => updateLaunchCommand(launchIdx, v)"
|
||||||
>
|
/>
|
||||||
<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="
|
|
||||||
$t('library.admin.import.version.launchPlaceholder')
|
|
||||||
"
|
|
||||||
@change="launchProcessQuery = $event.target.value"
|
|
||||||
@blur="launchProcessQuery = ''"
|
|
||||||
/>
|
|
||||||
<ComboboxButton
|
|
||||||
v-if="launchFilteredVersionGuesses?.length ?? 0 > 0"
|
|
||||||
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
|
||||||
>
|
|
||||||
<ChevronUpDownIcon
|
|
||||||
class="size-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</ComboboxButton>
|
|
||||||
|
|
||||||
<ComboboxOptions
|
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
|
|
||||||
>
|
|
||||||
<ComboboxOption
|
|
||||||
v-for="guess in launchFilteredVersionGuesses"
|
|
||||||
:key="guess.filename"
|
|
||||||
v-slot="{ active, selected }"
|
|
||||||
:value="guess.filename"
|
|
||||||
as="template"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
:class="[
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
|
||||||
active
|
|
||||||
? 'bg-blue-600 text-white outline-none'
|
|
||||||
: 'text-zinc-100',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center gap-x-2 block truncate',
|
|
||||||
selected && 'font-semibold',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ guess.filename }}
|
|
||||||
<IconsPlatform
|
|
||||||
:platform="guess.platform.platformIcon.key"
|
|
||||||
:fallback="guess.platform.platformIcon.fallback"
|
|
||||||
class="size-5 flex-shrink-0 text-blue-600"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
:class="[
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
<ComboboxOption
|
|
||||||
v-if="launchProcessQuery"
|
|
||||||
v-slot="{ active, selected }"
|
|
||||||
:value="launchProcessQuery"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
:class="[
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
|
||||||
active
|
|
||||||
? 'bg-blue-600 text-white outline-none'
|
|
||||||
: 'text-zinc-100',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'block truncate',
|
|
||||||
selected && 'font-semibold',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ $t("chars.quoted", { text: launchProcessQuery }) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
:class="[
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
</ComboboxOptions>
|
|
||||||
</div>
|
|
||||||
</Combobox>
|
|
||||||
<input
|
<input
|
||||||
id="startup"
|
id="startup"
|
||||||
v-model="launch.launchArgs"
|
v-model="launch.launchArgs"
|
||||||
@ -439,6 +244,41 @@
|
|||||||
class="absolute inset-0 bg-zinc-900/50"
|
class="absolute inset-0 bg-zinc-900/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- uninstall command -->
|
||||||
|
<div class="max-w-lg">
|
||||||
|
<label
|
||||||
|
for="startup"
|
||||||
|
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||||
|
>Uninstall command</label
|
||||||
|
>
|
||||||
|
<p class="text-zinc-400 text-xs">
|
||||||
|
Executable to be run on uninstalling a game. Useful for installer-only games.
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{{ $t("library.admin.import.version.installDir") }}
|
||||||
|
</span>
|
||||||
|
<PreloadSelector
|
||||||
|
:value="versionSettings.uninstall"
|
||||||
|
:guesses="versionGuesses"
|
||||||
|
@update="(v) => updateUninstallCommand(v)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id="startup"
|
||||||
|
v-model="versionSettings.uninstallArgs"
|
||||||
|
type="text"
|
||||||
|
name="startup"
|
||||||
|
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||||
|
placeholder="--uninstall"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PlatformSelector
|
<PlatformSelector
|
||||||
v-model="versionSettings.platform"
|
v-model="versionSettings.platform"
|
||||||
@ -617,11 +457,6 @@ import {
|
|||||||
Disclosure,
|
Disclosure,
|
||||||
DisclosureButton,
|
DisclosureButton,
|
||||||
DisclosurePanel,
|
DisclosurePanel,
|
||||||
Combobox,
|
|
||||||
ComboboxButton,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxOption,
|
|
||||||
ComboboxOptions,
|
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
@ -641,9 +476,7 @@ const gameId = route.params.id.toString();
|
|||||||
const versions = await $dropFetch(
|
const versions = await $dropFetch(
|
||||||
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
||||||
);
|
);
|
||||||
const userPlatforms = await $dropFetch(
|
const userPlatforms = await useAdminPlatforms();
|
||||||
"/api/v1/admin/import/version/platforms",
|
|
||||||
);
|
|
||||||
const allPlatforms = renderPlatforms(userPlatforms);
|
const allPlatforms = renderPlatforms(userPlatforms);
|
||||||
const currentlySelectedVersion = ref(-1);
|
const currentlySelectedVersion = ref(-1);
|
||||||
const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
|
const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
|
||||||
@ -656,27 +489,19 @@ const versionGuesses =
|
|||||||
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const launchProcessQuery = ref("");
|
|
||||||
const setupProcessQuery = ref("");
|
|
||||||
|
|
||||||
const launchFilteredVersionGuesses = computed(() =>
|
|
||||||
versionGuesses.value?.filter((e) =>
|
|
||||||
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const setupFilteredVersionGuesses = computed(() =>
|
|
||||||
versionGuesses.value?.filter((e) =>
|
|
||||||
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateLaunchCommand(idx: number, value: string) {
|
function updateLaunchCommand(idx: number, value: string) {
|
||||||
versionSettings.value.launches![idx].launchCommand = value;
|
versionSettings.value.launches![idx].launchCommand = value;
|
||||||
autosetPlatform(value);
|
autosetPlatform(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSetupCommand(value: string) {
|
function updateInstallCommand(value: string) {
|
||||||
versionSettings.value.setup = value;
|
versionSettings.value.install = value;
|
||||||
|
autosetPlatform(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUninstallCommand(value: string) {
|
||||||
|
versionSettings.value.uninstall = value;
|
||||||
autosetPlatform(value);
|
autosetPlatform(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,15 @@ export default defineEventHandler(async (h3) => {
|
|||||||
omit: {
|
omit: {
|
||||||
dropletManifest: true,
|
dropletManifest: true,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
gameVersions: {
|
||||||
|
include: {
|
||||||
|
install: true,
|
||||||
|
uninstall: true,
|
||||||
|
launches: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tags: true,
|
tags: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,13 +18,15 @@ export const ImportVersion = type({
|
|||||||
name: "string?",
|
name: "string?",
|
||||||
|
|
||||||
platform: "string",
|
platform: "string",
|
||||||
setup: "string = ''",
|
|
||||||
setupArgs: "string = ''",
|
|
||||||
onlySetup: "boolean = false",
|
onlySetup: "boolean = false",
|
||||||
delta: "boolean = false",
|
delta: "boolean = false",
|
||||||
umuId: "string = ''",
|
umuId: "string = ''",
|
||||||
|
|
||||||
|
install: "string?",
|
||||||
|
installArgs: "string?",
|
||||||
launches: LaunchCommands,
|
launches: LaunchCommands,
|
||||||
|
uninstall: "string?",
|
||||||
|
uninstallArgs: "string?",
|
||||||
}).configure(throwingArktype);
|
}).configure(throwingArktype);
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
@ -56,10 +58,10 @@ export default defineEventHandler(async (h3) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (body.onlySetup) {
|
if (body.onlySetup) {
|
||||||
if (!body.setup)
|
if (!body.install)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Setup required in "setup mode".',
|
message: 'Install required in "setup mode".',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!body.delta && body.launches.length == 0)
|
if (!body.delta && body.launches.length == 0)
|
||||||
|
|||||||
@ -14,7 +14,10 @@ import { GameNotFoundError, type LibraryProvider } from "./provider";
|
|||||||
import { logger } from "../logging";
|
import { logger } from "../logging";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
|
||||||
import type { LaunchOptionCreateManyInput } from "~/prisma/client/models";
|
import type {
|
||||||
|
GameVersionCreateInput,
|
||||||
|
LaunchOptionCreateManyInput,
|
||||||
|
} from "~/prisma/client/models";
|
||||||
|
|
||||||
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
|
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
|
||||||
return createHash("md5")
|
return createHash("md5")
|
||||||
@ -244,8 +247,6 @@ class LibraryManager {
|
|||||||
fileExts[platform.id] = platform.fileExtensions;
|
fileExts[platform.id] = platform.fileExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(fileExts);
|
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
filename: string;
|
filename: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
@ -342,6 +343,28 @@ class LibraryManager {
|
|||||||
where: { version: { gameId: gameId } },
|
where: { version: { gameId: gameId } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const installCreator = {
|
||||||
|
install: {
|
||||||
|
create: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
command: metadata.install!,
|
||||||
|
args: metadata.installArgs || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Partial<GameVersionCreateInput>;
|
||||||
|
|
||||||
|
const uninstallCreator = {
|
||||||
|
uninstall: {
|
||||||
|
create: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
command: metadata.uninstall!,
|
||||||
|
args: metadata.uninstallArgs || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Partial<GameVersionCreateInput>;
|
||||||
|
|
||||||
// Then, create the database object
|
// Then, create the database object
|
||||||
await prisma.version.create({
|
await prisma.version.create({
|
||||||
data: {
|
data: {
|
||||||
@ -372,14 +395,8 @@ class LibraryManager {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
install: {
|
...(metadata.install ? installCreator : undefined),
|
||||||
create: {
|
...(metadata.uninstall ? uninstallCreator : undefined),
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
command: metadata.setup,
|
|
||||||
args: metadata.setupArgs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
platform: {
|
platform: {
|
||||||
connect: {
|
connect: {
|
||||||
|
|||||||
Reference in New Issue
Block a user