mirror of
https://github.com/BillyOutlast/drop.git
synced 2026-02-04 00:31:17 +01:00
@@ -1,5 +1,31 @@
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
include:
|
||||
- template: Jobs/Build.gitlab-ci.yml
|
||||
services:
|
||||
- docker:24.0.5-dind
|
||||
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:latest
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
|
||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
|
||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME .
|
||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
||||
- docker push $IMAGE_NAME
|
||||
- docker push $LATEST_IMAGE_NAME
|
||||
- |
|
||||
if [ $CI_COMMIT_TAG ]; then
|
||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
||||
fi
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "drop-base"]
|
||||
path = drop-base
|
||||
url = https://github.com/Drop-OSS/drop-base.git
|
||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -2,5 +2,17 @@
|
||||
"spellchecker.ignoreWordsList": [
|
||||
"mTLS",
|
||||
"Wireguard"
|
||||
],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 5432,
|
||||
"driver": "PostgreSQL",
|
||||
"name": "drop",
|
||||
"database": "drop",
|
||||
"username": "drop",
|
||||
"password": "drop"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
app.vue
1
app.vue
@@ -2,6 +2,7 @@
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<ModalStack />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
95
components/AddLibraryButton.vue
Normal file
95
components/AddLibraryButton.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex justify-center items-center gap-x-2 rounded-l-md aspect-[7/2] px-3 py-2 bg-blue-600 grow text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Add to Library
|
||||
<PlusIcon class="-mr-0.5 size-6" aria-hidden="true" />
|
||||
</button>
|
||||
<Menu as="div" class="relative inline-block text-left grow">
|
||||
<div class="h-full">
|
||||
<MenuButton
|
||||
class="inline-flex h-full w-full justify-center items-center rounded-r-md bg-blue-600 p-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"
|
||||
>
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
href="#"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-gray-100 text-gray-900 outline-none'
|
||||
: 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
]"
|
||||
>Account settings</a
|
||||
>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
href="#"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-gray-100 text-gray-900 outline-none'
|
||||
: 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
]"
|
||||
>Support</a
|
||||
>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
href="#"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-gray-100 text-gray-900 outline-none'
|
||||
: 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
]"
|
||||
>License</a
|
||||
>
|
||||
</MenuItem>
|
||||
<form method="POST" action="#">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button
|
||||
type="submit"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-gray-100 text-gray-900 outline-none'
|
||||
: 'text-gray-700',
|
||||
'block w-full px-4 py-2 text-left text-sm',
|
||||
]"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</MenuItem>
|
||||
</form>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/vue/20/solid";
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-row flex-wrap gap-3 justify-center">
|
||||
<div class="flex flex-row flex-wrap gap-2 justify-center">
|
||||
<button
|
||||
v-for="(_, i) in amount"
|
||||
@click="() => slideTo(i)"
|
||||
:class="[
|
||||
currentSlide == i ? 'bg-zinc-300' : 'bg-zinc-700',
|
||||
'cursor-pointer w-4 h-2 rounded-full',
|
||||
currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
'transition-all cursor-pointer h-2 rounded-full',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<img :src="useObject(game.mCoverId)" class="w-full h-full object-cover" />
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-95% to-zinc-950"
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-[100%] to-zinc-950/50"
|
||||
/>
|
||||
<div class="absolute bottom-0 left-0 px-2 py-1.5">
|
||||
<h1 class="text-zinc-100 text-sm font-bold font-display">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-9 items-center justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<div v-if="props.loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span v-else> <slot /> </span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ loading: boolean }>();
|
||||
</script>
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
|
||||
class="flex min-h-full items-start justify-center p-4 text-center sm:items-center sm:p-0"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
@@ -73,9 +73,6 @@
|
||||
@click="() => uploadFile_wrapper()"
|
||||
:class="[
|
||||
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
|
||||
currentFile === undefined
|
||||
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
|
||||
: 'text-white bg-blue-600 hover:bg-blue-500',
|
||||
]"
|
||||
>
|
||||
Upload
|
||||
@@ -121,8 +118,8 @@ import {
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||
import { ArrowUpTrayIcon } from "@heroicons/vue/20/solid";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const open = defineModel<boolean>();
|
||||
|
||||
|
||||
0
composables/collection.ts
Normal file
0
composables/collection.ts
Normal file
6
composables/icons.ts
Normal file
6
composables/icons.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IconsLinuxLogo, IconsWindowsLogo } from "#components";
|
||||
|
||||
export const PLATFORM_ICONS = {
|
||||
[PlatformClient.Linux]: IconsLinuxLogo,
|
||||
[PlatformClient.Windows]: IconsWindowsLogo,
|
||||
};
|
||||
@@ -2,45 +2,74 @@ import type { TaskMessage } from "~/server/internal/tasks";
|
||||
import { WebSocketHandler } from "./ws";
|
||||
|
||||
const websocketHandler = new WebSocketHandler("/api/v1/task");
|
||||
const taskStates: { [key: string]: Ref<TaskMessage | undefined> } = {};
|
||||
|
||||
function handleUpdateMessage(msg: TaskMessage) {
|
||||
const taskStates = useTaskStates();
|
||||
const state = taskStates[msg.id];
|
||||
if (!state) return;
|
||||
if (!state.value || msg.reset) {
|
||||
state.value = msg;
|
||||
return;
|
||||
}
|
||||
state.value.log.push(...msg.log);
|
||||
|
||||
Object.assign(state.value, { ...msg, log: state.value.log });
|
||||
}
|
||||
|
||||
websocketHandler.listen((message) => {
|
||||
const msg = JSON.parse(message) as TaskMessage;
|
||||
const taskStates = useTaskStates();
|
||||
const state = taskStates.value[msg.id];
|
||||
if (!state) return;
|
||||
state.value = msg;
|
||||
try {
|
||||
// If it's an object, it's an update message
|
||||
const msg = JSON.parse(message) as TaskMessage;
|
||||
handleUpdateMessage(msg);
|
||||
} catch {
|
||||
// Otherwise it's control message
|
||||
const taskStates = useTaskStates();
|
||||
|
||||
const [action, ...data] = message.split("/");
|
||||
|
||||
switch (action) {
|
||||
case "connect":
|
||||
const taskReady = useTaskReady();
|
||||
taskReady.value = true;
|
||||
break;
|
||||
case "disconnect":
|
||||
const disconnectTaskId = data[0];
|
||||
delete taskStates[disconnectTaskId];
|
||||
console.log(`disconnected from ${disconnectTaskId}`);
|
||||
break;
|
||||
case "error":
|
||||
const [taskId, title, description] = data;
|
||||
taskStates[taskId].value ??= {
|
||||
id: taskId,
|
||||
name: "Unknown task",
|
||||
success: false,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
};
|
||||
taskStates[taskId].value.error = { title, description };
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const useTaskStates = () =>
|
||||
useState<{ [key: string]: Ref<TaskMessage> }>("task-states", () => ({
|
||||
connect: useState<TaskMessage>("task-connect", () => ({
|
||||
id: "connect",
|
||||
name: "Connect",
|
||||
success: false,
|
||||
progress: 0,
|
||||
log: [],
|
||||
error: undefined,
|
||||
})),
|
||||
}));
|
||||
const useTaskStates = () => taskStates;
|
||||
|
||||
export const useTaskReady = () => {
|
||||
export const useTaskReady = () => useState("taskready", () => false);
|
||||
|
||||
export const useTask = (taskId: string): Ref<TaskMessage | undefined> => {
|
||||
if (import.meta.server) return ref(undefined);
|
||||
const taskStates = useTaskStates();
|
||||
return taskStates.value["connect"];
|
||||
};
|
||||
if (
|
||||
taskStates[taskId] &&
|
||||
taskStates[taskId].value &&
|
||||
!taskStates[taskId].value.error
|
||||
)
|
||||
return taskStates[taskId];
|
||||
|
||||
export const useTask = (taskId: string): Ref<TaskMessage> => {
|
||||
if (import.meta.server) return {} as unknown as Ref<TaskMessage>;
|
||||
const taskStates = useTaskStates();
|
||||
if (taskStates.value[taskId]) return taskStates.value[taskId];
|
||||
|
||||
taskStates.value[taskId] = useState(`task-${taskId}`, () => ({
|
||||
id: taskId,
|
||||
name: "loading...",
|
||||
success: false,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
}));
|
||||
taskStates[taskId] = ref(undefined);
|
||||
console.log("connecting to " + taskId);
|
||||
websocketHandler.send(`connect/${taskId}`);
|
||||
return taskStates.value[taskId];
|
||||
return taskStates[taskId];
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready", "-d", "postgres"]
|
||||
test: pg_isready -d drop -U drop
|
||||
interval: 30s
|
||||
timeout: 60s
|
||||
retries: 5
|
||||
|
||||
1
drop-base
Submodule
1
drop-base
Submodule
Submodule drop-base added at 01fd41c65a
@@ -1,33 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<TransitionRoot as="template" :show="sidebarOpen">
|
||||
<Dialog class="relative z-50 lg:hidden" @close="sidebarOpen = false">
|
||||
<TransitionChild as="template" enter="transition-opacity ease-linear duration-300" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="transition-opacity ease-linear duration-300" leave-from="opacity-100"
|
||||
leave-to="opacity-0">
|
||||
<div class="fixed inset-0 bg-zinc-900/80" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 flex">
|
||||
<TransitionChild as="template" enter="transition ease-in-out duration-300 transform"
|
||||
enter-from="-translate-x-full" enter-to="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform" leave-from="translate-x-0"
|
||||
leave-to="-translate-x-full">
|
||||
<DialogPanel class="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<TransitionChild as="template" enter="ease-in-out duration-300" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="ease-in-out duration-300" leave-from="opacity-100" leave-to="opacity-0">
|
||||
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button type="button" class="-m-2.5 p-2.5" @click="sidebarOpen = false">
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<XMarkIcon class="size-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-zinc-950 px-4 pb-4">
|
||||
<div class="inline-flex items-center py-4 px-4">
|
||||
<Wordmark class="h-full w-auto" alt="Drop`" />
|
||||
</div>
|
||||
<nav>
|
||||
<ul role="list" class="grid grid-cols-2 items-stretch gap-4 px-5">
|
||||
<li v-for="(item, itemIdx) in navigation" :key="item.route">
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentNavigationIndex
|
||||
? 'bg-zinc-900 text-white'
|
||||
: 'text-zinc-400 hover:bg-zinc-900 hover:text-white',
|
||||
'transition group flex flex-col items-center grow gap-x-3 rounded-md px-2 py-3 text-sm font-semibold leading-6',
|
||||
]">
|
||||
<component :is="item.icon" class="h-6 w-6 shrink-0" aria-hidden="true" />
|
||||
<span class="text-xs text-center">{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
|
||||
<!-- Static sidebar for desktop -->
|
||||
<div
|
||||
class="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:z-50 lg:block lg:w-20 lg:overflow-y-auto lg:bg-zinc-950 lg:pb-4"
|
||||
>
|
||||
class="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:z-50 lg:block lg:w-20 lg:overflow-y-auto lg:bg-zinc-950 lg:pb-4">
|
||||
<div class="flex flex-col h-24 shrink-0 items-center justify-center">
|
||||
<Logo class="h-8 w-auto" />
|
||||
<span
|
||||
class="mt-1 bg-blue-400 px-1 py-0.5 rounded-md text-xs font-bold font-display"
|
||||
>Admin</span
|
||||
>
|
||||
<span class="mt-1 bg-blue-400 px-1 py-0.5 rounded-md text-xs font-bold font-display">Admin</span>
|
||||
</div>
|
||||
<nav class="mt-8">
|
||||
<ul role="list" class="flex flex-col items-stretch space-y-4 mx-2">
|
||||
<li v-for="(item, itemIdx) in navigation" :key="item.route">
|
||||
<NuxtLink
|
||||
:href="item.route"
|
||||
:class="[
|
||||
itemIdx === currentNavigationIndex
|
||||
? 'bg-zinc-900 text-white'
|
||||
: 'text-zinc-400 hover:bg-zinc-900 hover:text-white',
|
||||
'transition group flex flex-col items-center grow gap-x-3 rounded-md px-2 py-3 text-sm font-semibold leading-6',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
class="h-6 w-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentNavigationIndex
|
||||
? 'bg-zinc-900 text-white'
|
||||
: 'text-zinc-400 hover:bg-zinc-900 hover:text-white',
|
||||
'transition group flex flex-col items-center grow gap-x-3 rounded-md px-2 py-3 text-sm font-semibold leading-6',
|
||||
]">
|
||||
<component :is="item.icon" class="h-6 w-6 shrink-0" aria-hidden="true" />
|
||||
<span class="text-xs text-center">{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
@@ -35,14 +74,8 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="sticky top-0 z-40 flex items-center gap-x-6 bg-zinc-900 px-4 py-4 shadow-sm sm:px-6 lg:hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<div class="sticky top-0 z-40 flex items-center gap-x-6 bg-zinc-900 px-4 py-4 shadow-sm sm:px-6 lg:hidden">
|
||||
<button type="button" class="-m-2.5 p-2.5 text-zinc-400 lg:hidden" @click="sidebarOpen = true">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -59,6 +92,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type Component } from "vue";
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from '@headlessui/vue'
|
||||
import {
|
||||
Bars3Icon,
|
||||
ServerStackIcon,
|
||||
@@ -70,6 +113,7 @@ import {
|
||||
import type { NavigationItem } from "~/composables/types";
|
||||
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
||||
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{ label: "Home", route: "/admin", prefix: "/admin", icon: HomeIcon },
|
||||
@@ -108,6 +152,10 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
|
||||
|
||||
const sidebarOpen = ref(false);
|
||||
const router = useRouter();
|
||||
router.afterEach(() => {
|
||||
sidebarOpen.value = false;
|
||||
})
|
||||
|
||||
useHead({
|
||||
titleTemplate(title) {
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||
<UserHeader class="z-50" />
|
||||
<div class="grow flex">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
<UserFooter class="z-50" />
|
||||
</div>
|
||||
<div class="flex w-full min-h-screen bg-zinc-900" v-else>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const noWrapper = !!route.query.noWrapper;
|
||||
|
||||
useHead({
|
||||
titleTemplate(title) {
|
||||
if (title) return `${title} | Drop`;
|
||||
|
||||
@@ -32,6 +32,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
extends: ['./drop-base'],
|
||||
|
||||
// Module config from here down
|
||||
modules: ["@nuxt/content", "vue3-carousel-nuxt"],
|
||||
|
||||
@@ -54,4 +56,4 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxt/content": "^2.13.4",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
||||
@@ -382,7 +382,6 @@ import {
|
||||
import type { Invitation } from "@prisma/client";
|
||||
import moment from "moment";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import LoadingButton from "~/components/LoadingButton.vue";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
@@ -434,7 +433,7 @@ const email = computed({
|
||||
_email.value = v;
|
||||
},
|
||||
});
|
||||
const mailRegex = /^\S+@\S+\.\S+$/g;
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
const validEmail = computed(() =>
|
||||
_email.value === undefined ? true : mailRegex.test(email.value as string)
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div class="flex flex-col gap-y-4 max-w-lg">
|
||||
<Listbox
|
||||
as="div"
|
||||
class="max-w-md"
|
||||
v-on:update:model-value="(value) => updateCurrentlySelectedVersion(value)"
|
||||
:model-value="currentlySelectedVersion"
|
||||
>
|
||||
@@ -74,33 +73,8 @@
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-md" v-if="versionSettings">
|
||||
<div>
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Startup executable/command</label
|
||||
>
|
||||
<p class="text-zinc-400 text-xs">Executable to launch the game</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex 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 sm:max-w-md"
|
||||
>
|
||||
<span
|
||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||
>(install_dir)/</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="startup"
|
||||
id="startup"
|
||||
v-model="versionSettings.startup"
|
||||
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="my-game.exe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-8" v-if="versionGuesses">
|
||||
<!-- setup executable -->
|
||||
<div>
|
||||
<label
|
||||
for="startup"
|
||||
@@ -110,23 +84,282 @@
|
||||
<p class="text-zinc-400 text-xs">Ran once when the game is installed</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex 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 sm:max-w-md"
|
||||
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"
|
||||
>(install_dir)/</span
|
||||
>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="versionSettings.setup"
|
||||
@update:model-value="(v) => updateSetupCommand(v)"
|
||||
nullable
|
||||
>
|
||||
<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"
|
||||
@change="setupProcessQuery = $event.target.value"
|
||||
@blur="setupProcessQuery = ''"
|
||||
:placeholder="'setup.exe'"
|
||||
/>
|
||||
<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 setupFilteredVersionGuesses"
|
||||
:key="guess.filename"
|
||||
:value="guess.filename"
|
||||
as="template"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<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 }}
|
||||
<component
|
||||
:is="PLATFORM_ICONS[guess.platform as PlatformClient]"
|
||||
class="size-5"
|
||||
/>
|
||||
</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
|
||||
:value="launchProcessQuery"
|
||||
v-if="launchProcessQuery"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-gray-900',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
"{{ 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
|
||||
type="text"
|
||||
name="startup"
|
||||
id="startup"
|
||||
v-model="versionSettings.setup"
|
||||
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="setup.exe"
|
||||
v-model="versionSettings.setupArgs"
|
||||
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="--setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- setup mode -->
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Setup mode</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>When enabled, this version does not have a launch command, and
|
||||
simply runs the executable on the user's computer. Useful for games
|
||||
that only distribute installer and not portable
|
||||
files.</SwitchDescription
|
||||
>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="versionSettings.onlySetup"
|
||||
:class="[
|
||||
versionSettings.onlySetup ? '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="[
|
||||
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 class="relative">
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Launch executable/command</label
|
||||
>
|
||||
<p class="text-zinc-400 text-xs">Executable to launch the game</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"
|
||||
>(install_dir)/</span
|
||||
>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="versionSettings.launch"
|
||||
@update:model-value="(v) => updateLaunchCommand(v)"
|
||||
nullable
|
||||
>
|
||||
<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"
|
||||
@change="launchProcessQuery = $event.target.value"
|
||||
@blur="launchProcessQuery = ''"
|
||||
:placeholder="'game.exe'"
|
||||
/>
|
||||
<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"
|
||||
:value="guess.filename"
|
||||
as="template"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<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 }}
|
||||
<component
|
||||
:is="PLATFORM_ICONS[guess.platform as PlatformClient]"
|
||||
class="size-5"
|
||||
/>
|
||||
</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
|
||||
:value="launchProcessQuery"
|
||||
v-if="launchProcessQuery"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-none'
|
||||
: 'text-gray-900',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
"{{ 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
|
||||
type="text"
|
||||
name="startup"
|
||||
id="startup"
|
||||
v-model="versionSettings.launchArgs"
|
||||
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="--launch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 bg-zinc-900/50"
|
||||
v-if="versionSettings.onlySetup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PlatformSelector v-model="versionSettings.platform">
|
||||
Version platform
|
||||
</PlatformSelector>
|
||||
@@ -145,16 +378,16 @@
|
||||
>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="delta"
|
||||
v-model="versionSettings.delta"
|
||||
:class="[
|
||||
delta ? 'bg-blue-600' : 'bg-zinc-800',
|
||||
versionSettings.delta ? '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="[
|
||||
delta ? 'translate-x-5' : 'translate-x-0',
|
||||
versionSettings.delta ? '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',
|
||||
]"
|
||||
/>
|
||||
@@ -163,7 +396,7 @@
|
||||
<Disclosure as="div" class="py-2" v-slot="{ open }">
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="flex w-full items-start justify-between text-left text-zinc-100"
|
||||
class="border-b border-zinc-600 pb-2 flex w-full items-start justify-between text-left text-zinc-100"
|
||||
>
|
||||
<span class="text-base/7 font-semibold">Advanced options</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
@@ -172,57 +405,70 @@
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-4">
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Override UMU Launcher Game ID</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>By default, Drop uses a non-ID when launching with UMU
|
||||
Launcher. In order to get the right patches for some games, you
|
||||
may have to manually set this field.</SwitchDescription
|
||||
>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="umuIdEnabled"
|
||||
:class="[
|
||||
umuIdEnabled ? '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"
|
||||
<DisclosurePanel
|
||||
as="dd"
|
||||
class="bg-zinc-950/30 p-3 rounded-b-lg mt-2 flex flex-col gap-y-4"
|
||||
>
|
||||
<!-- UMU launcher configuration -->
|
||||
<div
|
||||
v-if="versionSettings.platform == PlatformClient.Windows"
|
||||
class="flex flex-col gap-y-4"
|
||||
>
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<SwitchLabel
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Override UMU Launcher Game ID</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>By default, Drop uses a non-ID when launching with UMU
|
||||
Launcher. In order to get the right patches for some games,
|
||||
you may have to manually set this field.</SwitchDescription
|
||||
>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="umuIdEnabled"
|
||||
:class="[
|
||||
umuIdEnabled ? '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',
|
||||
umuIdEnabled ? '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',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
<div>
|
||||
<label
|
||||
for="umu-id"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>UMU Launcher ID</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="umu-id"
|
||||
name="umu-id"
|
||||
type="text"
|
||||
autocomplete="umu-id"
|
||||
required
|
||||
:disabled="!umuIdEnabled"
|
||||
v-model="umuId"
|
||||
placeholder="umu-starcitizen"
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
umuIdEnabled ? '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>
|
||||
<label
|
||||
for="umu-id"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>UMU Launcher ID</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="umu-id"
|
||||
name="umu-id"
|
||||
type="text"
|
||||
autocomplete="umu-id"
|
||||
required
|
||||
:disabled="!umuIdEnabled"
|
||||
v-model="umuId"
|
||||
placeholder="umu-starcitizen"
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 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-else class="text-zinc-400">
|
||||
No advanced options for this configuration.
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
|
||||
@@ -287,6 +533,12 @@ import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxLabel,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
@@ -308,21 +560,72 @@ const versions = await $fetch(
|
||||
}
|
||||
);
|
||||
const currentlySelectedVersion = ref(-1);
|
||||
const versionSettings = ref<
|
||||
{ platform: string; startup: string; setup: string } | undefined
|
||||
>();
|
||||
const delta = ref(false);
|
||||
const versionSettings = ref<{
|
||||
platform: string;
|
||||
|
||||
onlySetup: boolean;
|
||||
launch: string;
|
||||
launchArgs: string;
|
||||
setup: string;
|
||||
setupArgs: string;
|
||||
|
||||
delta: boolean;
|
||||
umuId: string;
|
||||
}>({
|
||||
platform: "",
|
||||
launch: "",
|
||||
launchArgs: "",
|
||||
setup: "",
|
||||
setupArgs: "",
|
||||
delta: false,
|
||||
onlySetup: false,
|
||||
umuId: "",
|
||||
});
|
||||
|
||||
const versionGuesses = ref<Array<{ platform: string; 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(value: string) {
|
||||
versionSettings.value.launch = value;
|
||||
autosetPlatform(value);
|
||||
}
|
||||
|
||||
function updateSetupCommand(value: string) {
|
||||
versionSettings.value.setup = value;
|
||||
autosetPlatform(value);
|
||||
}
|
||||
|
||||
function autosetPlatform(value: string) {
|
||||
if (!versionGuesses.value) return;
|
||||
if (versionSettings.value.platform) return;
|
||||
const guessIndex = versionGuesses.value.findIndex(
|
||||
(e) => e.filename === value
|
||||
);
|
||||
if (guessIndex == -1) return;
|
||||
versionSettings.value.platform = versionGuesses.value[guessIndex].platform;
|
||||
}
|
||||
|
||||
const _umuId = ref("");
|
||||
const umuIdEnabled = ref(false);
|
||||
const umuId = computed({
|
||||
get() {
|
||||
if (umuIdEnabled.value) return _umuId.value;
|
||||
if (umuIdEnabled.value) return versionSettings.value.umuId;
|
||||
return undefined;
|
||||
},
|
||||
set(v) {
|
||||
if (umuIdEnabled.value && v) {
|
||||
_umuId.value = v;
|
||||
versionSettings.value.umuId = v;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -339,11 +642,7 @@ async function updateCurrentlySelectedVersion(value: number) {
|
||||
gameId
|
||||
)}&version=${encodeURIComponent(version)}`
|
||||
);
|
||||
versionSettings.value = {
|
||||
platform: results.platformGuess,
|
||||
startup: results.startupGuess,
|
||||
setup: "",
|
||||
};
|
||||
versionGuesses.value = results;
|
||||
}
|
||||
|
||||
async function startImport() {
|
||||
@@ -353,10 +652,7 @@ async function startImport() {
|
||||
body: {
|
||||
id: gameId,
|
||||
version: versions[currentlySelectedVersion.value],
|
||||
platform: versionSettings.value.platform,
|
||||
startup: versionSettings.value.startup,
|
||||
setup: versionSettings.value.setup,
|
||||
delta: delta.value,
|
||||
...versionSettings.value,
|
||||
},
|
||||
});
|
||||
router.push(`/admin/task/${taskId.taskId}`);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<Listbox
|
||||
as="div"
|
||||
class="max-w-md"
|
||||
v-on:update:model-value="(value) => updateSelectedGame(value)"
|
||||
:model="currentlySelectedGame"
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Select game to import</ListboxLabel
|
||||
>
|
||||
<div class="flex flex-col gap-y-6 w-full max-w-md">
|
||||
<Listbox as="div" v-on:update:model-value="(value) => updateSelectedGame_wrapper(value)"
|
||||
:model="currentlySelectedGame">
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game to import</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<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-600 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-600 sm:text-sm sm:leading-6">
|
||||
<span v-if="currentlySelectedGame != -1" class="block truncate">{{
|
||||
games.unimportedGames[currentlySelectedGame]
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-400"
|
||||
>Please select a directory...</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else class="block truncate text-zinc-400">Please select a directory...</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
<ListboxOptions
|
||||
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-zinc-800 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(game, gameIdx) in games.unimportedGames"
|
||||
:key="game"
|
||||
:value="gameIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ game }}</span
|
||||
>
|
||||
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-zinc-800 focus:outline-none sm:text-sm">
|
||||
<ListboxOption as="template" v-for="(game, gameIdx) in games.unimportedGames" :key="game" :value="gameIdx"
|
||||
v-slot="{ active, selected }">
|
||||
<li :class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]">
|
||||
<span :class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]">{{ game }}</span>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<span v-if="selected" :class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]">
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
@@ -74,106 +43,97 @@
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
<Listbox
|
||||
as="div"
|
||||
class="max-w-md"
|
||||
v-if="metadataResults"
|
||||
v-model="currentlySelectedMetadata"
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Select game</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<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-600 sm:text-sm sm:leading-6"
|
||||
>
|
||||
<GameSearchResultWidget
|
||||
v-if="currentlySelectedMetadata != -1"
|
||||
:game="metadataResults[currentlySelectedMetadata]"
|
||||
/>
|
||||
<span v-else class="block truncate text-zinc-600"
|
||||
>Please select a game...</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4">
|
||||
<!-- without metadata option -->
|
||||
<div>
|
||||
<LoadingButton @click="() => importGame_wrapper(false)" class="w-fit" :loading="importLoading">Import without
|
||||
metadata
|
||||
</LoadingButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
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-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(result, resultIdx) in metadataResults"
|
||||
:key="result.id"
|
||||
:value="resultIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<GameSearchResultWidget :game="result" />
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
<div
|
||||
v-else-if="currentlySelectedGame != -1"
|
||||
role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
|
||||
>
|
||||
Loading game results...
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="currentlySelectedGame !== -1 && currentlySelectedMetadata !== -1">
|
||||
<LoadingButton
|
||||
@click="() => importGame_wrapper()"
|
||||
class="w-fit"
|
||||
:loading="importLoading"
|
||||
>Import</LoadingButton
|
||||
>
|
||||
<!-- divider -->
|
||||
<div class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold">
|
||||
<div class="h-[1px] grow bg-zinc-800" />OR
|
||||
<div class="h-[1px] grow bg-zinc-800" />
|
||||
</div>
|
||||
|
||||
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
<!-- with metadata option -->
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<Listbox as="div" v-if="metadataResults && metadataResults.length > 0" v-model="currentlySelectedMetadata">
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<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-600 sm:text-sm sm:leading-6">
|
||||
<GameSearchResultWidget v-if="currentlySelectedMetadata != -1"
|
||||
:game="metadataResults[currentlySelectedMetadata]" />
|
||||
<span v-else class="block truncate text-zinc-600">Please select a game...</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
<ListboxOptions
|
||||
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-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
<ListboxOption as="template" v-for="(result, resultIdx) in metadataResults" :key="result.id"
|
||||
:value="resultIdx" v-slot="{ active, selected }">
|
||||
<li :class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]">
|
||||
<GameSearchResultWidget :game="result" />
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ importError }}
|
||||
</h3>
|
||||
</Listbox>
|
||||
<div v-else-if="gameSearchResultsLoading" role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4">
|
||||
Loading game results...
|
||||
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-white" viewBox="0 0 100 101"
|
||||
fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="gameSearchResultsError" class="w-fit rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ gameSearchResultsError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LoadingButton @click="() => importGame_wrapper()" class="w-fit" :loading="importLoading"
|
||||
:disabled="currentlySelectedMetadata === -1">Import
|
||||
</LoadingButton>
|
||||
|
||||
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ importError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,6 +161,8 @@ const headers = useRequestHeaders(["cookie"]);
|
||||
const games = await $fetch("/api/v1/admin/import/game", { headers });
|
||||
|
||||
const currentlySelectedGame = ref(-1);
|
||||
const gameSearchResultsLoading = ref(false);
|
||||
const gameSearchResultsError = ref<string | undefined>();
|
||||
|
||||
async function updateSelectedGame(value: number) {
|
||||
if (currentlySelectedGame.value == value) return;
|
||||
@@ -218,6 +180,15 @@ async function updateSelectedGame(value: number) {
|
||||
metadataResults.value = results;
|
||||
}
|
||||
|
||||
function updateSelectedGame_wrapper(value: number) {
|
||||
gameSearchResultsLoading.value = true;
|
||||
updateSelectedGame(value).catch((error) => {
|
||||
gameSearchResultsError.value = error.statusMessage || "An unknown error occurred";
|
||||
}).finally(() => {
|
||||
gameSearchResultsLoading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const metadataResults = ref<Array<GameMetadataSearchResult> | undefined>();
|
||||
const currentlySelectedMetadata = ref(-1);
|
||||
|
||||
@@ -225,23 +196,23 @@ const router = useRouter();
|
||||
|
||||
const importLoading = ref(false);
|
||||
const importError = ref<string | undefined>();
|
||||
async function importGame() {
|
||||
async function importGame(metadata: boolean) {
|
||||
if (!metadataResults.value) return;
|
||||
|
||||
const game = await $fetch("/api/v1/admin/import/game", {
|
||||
method: "POST",
|
||||
body: {
|
||||
path: games.unimportedGames[currentlySelectedGame.value],
|
||||
metadata: metadataResults.value[currentlySelectedMetadata.value],
|
||||
metadata: metadata ? metadataResults.value[currentlySelectedMetadata.value] : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
router.push(`/admin/library/${game.id}`);
|
||||
}
|
||||
function importGame_wrapper() {
|
||||
function importGame_wrapper(metadata = true) {
|
||||
importLoading.value = true;
|
||||
importError.value = undefined;
|
||||
importGame()
|
||||
importGame(metadata)
|
||||
.catch((error) => {
|
||||
importError.value = error?.statusMessage || "An unknown error occurred.";
|
||||
})
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
>
|
||||
<div class="flex flex-1 flex-row p-4 gap-x-4">
|
||||
<img
|
||||
class="mx-auto h-16 w-16 flex-shrink-0 rounded-md"
|
||||
class="h-16 w-16 flex-shrink-0 rounded-md"
|
||||
:src="useObject(game.mIconId)"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="grow w-full flex items-center justify-center"
|
||||
v-if="taskValue && taskValue.success"
|
||||
v-if="task && task.success"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
|
||||
@@ -11,37 +11,78 @@
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-md">
|
||||
"{{ taskValue.name }}" completed successfully.
|
||||
"{{ task.name }}" completed successfully.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="taskValue" class="flex flex-col w-full gap-y-4">
|
||||
<div
|
||||
class="grow w-full flex items-center justify-center"
|
||||
v-else-if="task && task.error"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<ExclamationCircleIcon
|
||||
class="h-12 w-12 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
{{ task.error.title }}
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-md">
|
||||
{{ task.error.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="task" class="flex flex-col w-full gap-y-4">
|
||||
<h1 class="text-3xl text-zinc-100 font-bold font-display">
|
||||
{{ taskValue.name }}
|
||||
{{ task.name }}
|
||||
</h1>
|
||||
<div class="h-3 rounded-full bg-zinc-950 overflow-hidden">
|
||||
<div
|
||||
:style="{ width: `${taskValue.progress}%` }"
|
||||
:style="{ width: `${task.progress}%` }"
|
||||
class="transition-all bg-blue-600 h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-950/50 rounded-md p-2 text-zinc-100">
|
||||
<pre v-for="line in taskValue.log">{{ line }}</pre>
|
||||
<pre v-for="line in task.log">{{ line }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else role="status" class="w-full h-screen flex items-center justify-center">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-8 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const route = useRoute();
|
||||
const taskId = route.params.id.toString();
|
||||
|
||||
const task = useTask(taskId);
|
||||
const taskValue = computed(() => task.value);
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
|
||||
@@ -10,10 +10,38 @@
|
||||
Successful!
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-sm">
|
||||
<p class="mx-auto text-sm text-zinc-400 max-w-sm">
|
||||
Drop has successfully authorized the client. You may now close this
|
||||
window.
|
||||
</p>
|
||||
|
||||
<Disclosure as="div" class="mt-8" v-slot="{ open }">
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="pb-2 flex w-full items-start justify-between text-left text-zinc-400"
|
||||
>
|
||||
<span class="text-sm font-semibold">Having issues?</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<ChevronUpIcon
|
||||
v-if="!open"
|
||||
class="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ChevronDownIcon v-else class="size-4" aria-hidden="true" />
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2">
|
||||
<p class="text-zinc-100 font-semibold text-sm mb-3">
|
||||
Paste this code into the client to continue:
|
||||
</p>
|
||||
<p
|
||||
class="max-w-sm text-nowrap overflow-x-auto text-sm bg-zinc-950/50 p-3 text-zinc-300 w-fit mx-auto rounded-xl"
|
||||
>
|
||||
{{ authToken }}
|
||||
</p>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,6 +151,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
UserGroupIcon,
|
||||
@@ -130,6 +159,7 @@ import {
|
||||
} from "@heroicons/vue/16/solid";
|
||||
import { LockClosedIcon } from "@heroicons/vue/20/solid";
|
||||
import { CheckCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const route = useRoute();
|
||||
const clientId = route.params.id;
|
||||
@@ -142,12 +172,14 @@ const clientData = await useFetch(
|
||||
|
||||
const completed = ref(false);
|
||||
const error = ref();
|
||||
const authToken = ref<string | undefined>();
|
||||
|
||||
async function authorize() {
|
||||
const redirect = await $fetch<string>("/api/v1/client/auth/callback", {
|
||||
const { redirect, token } = await $fetch("/api/v1/client/auth/callback", {
|
||||
method: "POST",
|
||||
body: { id: clientId },
|
||||
});
|
||||
authToken.value = token;
|
||||
window.location.replace(redirect);
|
||||
}
|
||||
|
||||
|
||||
@@ -208,11 +208,11 @@ const username = ref(invitation.data.value?.username);
|
||||
const password = ref("");
|
||||
const confirmPassword = ref(undefined);
|
||||
|
||||
const mailRegex = /^\S+@\S+\.\S+$/g;
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
const validEmail = computed(() => mailRegex.test(email.value ?? ""));
|
||||
const validUsername = computed(
|
||||
() =>
|
||||
(username.value?.length ?? 0) > 5 &&
|
||||
(username.value?.length ?? 0) >= 5 &&
|
||||
username.value?.toLowerCase() == username.value
|
||||
);
|
||||
const validPassword = computed(() => (password.value?.length ?? 0) >= 14);
|
||||
|
||||
@@ -121,7 +121,6 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/20/solid";
|
||||
import type { User } from "@prisma/client";
|
||||
import LoadingButton from "~/components/LoadingButton.vue";
|
||||
import Logo from "~/components/Logo.vue";
|
||||
|
||||
const username = ref("");
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
>
|
||||
<component
|
||||
v-for="platform in platforms"
|
||||
:is="icons[platform]"
|
||||
:is="PLATFORM_ICONS[platform]"
|
||||
class="text-blue-600 w-6 h-6"
|
||||
/>
|
||||
<span
|
||||
@@ -113,12 +113,17 @@
|
||||
</p>
|
||||
<div class="mt-6 py-4 rounded">
|
||||
<VueCarousel :items-to-show="1">
|
||||
<VueSlide v-for="image in game.mImageLibrary" :key="image">
|
||||
<VueSlide v-for="image in game.mImageCarousel" :key="image">
|
||||
<img
|
||||
class="w-fit h-48 lg:h-96 rounded"
|
||||
:src="useObject(image)"
|
||||
/>
|
||||
</VueSlide>
|
||||
<VueSlide v-if="game.mImageCarousel.length == 0">
|
||||
<div class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display">
|
||||
No images
|
||||
</div>
|
||||
</VueSlide>
|
||||
|
||||
<template #addons>
|
||||
<VueNavigation />
|
||||
@@ -212,10 +217,7 @@ const rating = Math.round(game.mReviewRating * 5);
|
||||
const ratingArray = Array(5)
|
||||
.fill(null)
|
||||
.map((_, i) => i + 1 <= rating);
|
||||
const icons = {
|
||||
[PlatformClient.Linux]: IconsLinuxLogo,
|
||||
[PlatformClient.Windows]: IconsWindowsLogo,
|
||||
};
|
||||
|
||||
|
||||
useHead({
|
||||
title: game.mName,
|
||||
|
||||
@@ -33,11 +33,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
>
|
||||
<div
|
||||
class="relative mx-auto flex max-w-xl flex-col items-center text-center"
|
||||
>
|
||||
<div class="relative text-center">
|
||||
<h3 class="text-base/7 font-semibold text-blue-300">
|
||||
Recently added
|
||||
</h3>
|
||||
@@ -49,11 +47,20 @@
|
||||
<p class="mt-3 text-lg text-zinc-300 line-clamp-2">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
class="mt-8 block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
|
||||
>Check it out</NuxtLink
|
||||
>
|
||||
<div class="mt-8 gap-x-4 inline-flex items-center">
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
class="block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
|
||||
>Check it out</NuxtLink
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm hover:bg-zinc-900/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-100"
|
||||
>
|
||||
Add to Library
|
||||
<PlusIcon class="-mr-0.5 h-7 w-7" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,6 +110,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const headers = useRequestHeaders(["cookie"]);
|
||||
const recent = await $fetch("/api/v1/store/recent", { headers });
|
||||
const updated = await $fetch("/api/v1/store/updated", { headers });
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The values [Custom] on the enum `MetadataSource` will be removed. If these variants are still used in the database, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "MetadataSource_new" AS ENUM ('Manual', 'GiantBomb');
|
||||
ALTER TABLE "Game" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new");
|
||||
ALTER TABLE "Developer" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new");
|
||||
ALTER TABLE "Publisher" ALTER COLUMN "metadataSource" TYPE "MetadataSource_new" USING ("metadataSource"::text::"MetadataSource_new");
|
||||
ALTER TYPE "MetadataSource" RENAME TO "MetadataSource_old";
|
||||
ALTER TYPE "MetadataSource_new" RENAME TO "MetadataSource";
|
||||
DROP TYPE "MetadataSource_old";
|
||||
COMMIT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Game" ADD COLUMN "mImageCarousel" INTEGER[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Game" ALTER COLUMN "mImageCarousel" SET DATA TYPE TEXT[];
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "GameVersion" ADD COLUMN "launchArgs" TEXT[],
|
||||
ADD COLUMN "onlySetup" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "setupArgs" TEXT[],
|
||||
ALTER COLUMN "launchCommand" DROP NOT NULL,
|
||||
ALTER COLUMN "setupCommand" DROP NOT NULL;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Collection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Collection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_CollectionToGame" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_CollectionToGame_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_CollectionToGame_B_index" ON "_CollectionToGame"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Collection" ADD CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CollectionToGame" ADD CONSTRAINT "_CollectionToGame_A_fkey" FOREIGN KEY ("A") REFERENCES "Collection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CollectionToGame" ADD CONSTRAINT "_CollectionToGame_B_fkey" FOREIGN KEY ("B") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `_CollectionToGame` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_CollectionToGame" DROP CONSTRAINT "_CollectionToGame_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_CollectionToGame" DROP CONSTRAINT "_CollectionToGame_B_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_CollectionToGame";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CollectionEntry" (
|
||||
"collectionId" TEXT NOT NULL,
|
||||
"gameId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "CollectionEntry_pkey" PRIMARY KEY ("collectionId","gameId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
20
prisma/schema/collection.prisma
Normal file
20
prisma/schema/collection.prisma
Normal file
@@ -0,0 +1,20 @@
|
||||
model Collection {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
|
||||
isDefault Boolean @default(false)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
entries CollectionEntry[]
|
||||
}
|
||||
|
||||
model CollectionEntry {
|
||||
collectionId String
|
||||
collection Collection @relation(fields: [collectionId], references: [id])
|
||||
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id])
|
||||
|
||||
@@id([collectionId, gameId])
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
enum MetadataSource {
|
||||
Custom
|
||||
Manual
|
||||
GiantBomb
|
||||
}
|
||||
|
||||
@@ -22,13 +22,16 @@ model Game {
|
||||
mReviewCount Int
|
||||
mReviewRating Float // 0 to 1
|
||||
|
||||
mIconId String // linked to objects in s3
|
||||
mBannerId String // linked to objects in s3
|
||||
mCoverId String
|
||||
mImageLibrary String[] // linked to objects in s3
|
||||
mIconId String // linked to objects in s3
|
||||
mBannerId String // linked to objects in s3
|
||||
mCoverId String
|
||||
mImageCarousel String[] // linked to below array
|
||||
mImageLibrary String[] // linked to objects in s3
|
||||
|
||||
versions GameVersion[]
|
||||
libraryBasePath String @unique // Base dir for all the game versions
|
||||
libraryBasePath String @unique // Base dir for all the game versions
|
||||
|
||||
collections CollectionEntry[]
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
}
|
||||
@@ -41,10 +44,16 @@ model GameVersion {
|
||||
|
||||
created DateTime @default(now())
|
||||
|
||||
platform Platform
|
||||
launchCommand String // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
||||
setupCommand String // Command to setup game (dependencies and such)
|
||||
umuIdOverride String?
|
||||
platform Platform
|
||||
|
||||
launchCommand String? // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
||||
launchArgs String[]
|
||||
setupCommand String? // Command to setup game (dependencies and such)
|
||||
setupArgs String[]
|
||||
onlySetup Boolean @default(false)
|
||||
|
||||
umuIdOverride String?
|
||||
|
||||
dropletManifest Json // Results from droplet
|
||||
|
||||
versionIndex Int
|
||||
|
||||
@@ -12,4 +12,4 @@ generator client {
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ model User {
|
||||
displayName String
|
||||
profilePicture String // Object
|
||||
|
||||
authMecs LinkedAuthMec[]
|
||||
clients Client[]
|
||||
|
||||
authMecs LinkedAuthMec[]
|
||||
clients Client[]
|
||||
notifications Notification[]
|
||||
collections Collection[]
|
||||
}
|
||||
|
||||
model Notification {
|
||||
@@ -21,7 +21,7 @@ model Notification {
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
created DateTime @default(now())
|
||||
created DateTime @default(now())
|
||||
title String
|
||||
description String
|
||||
actions String[]
|
||||
|
||||
51
server/api/v1/admin/game/metadata.post.ts
Normal file
51
server/api/v1/admin/game/metadata.post.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await h3.context.session.getAdminUser(h3);
|
||||
if (!user) throw createError({ statusCode: 403 });
|
||||
|
||||
const form = await readMultipartFormData(h3);
|
||||
if (!form)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "This endpoint requires multipart form data.",
|
||||
});
|
||||
|
||||
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]);
|
||||
if (!uploadResult)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Failed to upload file",
|
||||
});
|
||||
|
||||
const [id, options, pull, dump] = uploadResult;
|
||||
|
||||
// handleFileUpload reads the rest of the options for us.
|
||||
const name = options.name;
|
||||
const description = options.description;
|
||||
const gameId = options.id;
|
||||
|
||||
if (!(id || name || description)) {
|
||||
dump();
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Nothing has changed",
|
||||
});
|
||||
}
|
||||
|
||||
await pull();
|
||||
const newObject = await prisma.game.update({
|
||||
where: {
|
||||
id: gameId,
|
||||
},
|
||||
data: {
|
||||
mIconId: id,
|
||||
mName: name,
|
||||
mShortDescription: description,
|
||||
},
|
||||
});
|
||||
|
||||
return newObject;
|
||||
});
|
||||
@@ -18,11 +18,6 @@ export default defineEventHandler(async (h3) => {
|
||||
statusCode: 400,
|
||||
statusMessage: "Path missing from body",
|
||||
});
|
||||
if (!metadata.id || !metadata.sourceId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Metadata IDs missing from body",
|
||||
});
|
||||
|
||||
const validPath = await libraryManager.checkUnimportedGamePath(path);
|
||||
if (!validPath)
|
||||
@@ -31,6 +26,9 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Invalid unimported game path",
|
||||
});
|
||||
|
||||
const game = await h3.context.metadataHandler.createGame(metadata, path);
|
||||
return game;
|
||||
if (!metadata || !metadata.id || !metadata.sourceId) {
|
||||
return await h3.context.metadataHandler.createGameWithoutMetadata(path);
|
||||
} else {
|
||||
return await h3.context.metadataHandler.createGame(metadata, path);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,5 +9,13 @@ export default defineEventHandler(async (h3) => {
|
||||
if (!search)
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid search" });
|
||||
|
||||
return await h3.context.metadataHandler.search(search);
|
||||
const results = await h3.context.metadataHandler.search(search);
|
||||
|
||||
if (results.length == 0)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "No metadata provider returned search results.",
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
import { parsePlatform } from "~/server/internal/utils/parseplatform";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await h3.context.session.getAdminUser(h3);
|
||||
@@ -8,35 +9,32 @@ export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
const gameId = body.id;
|
||||
const versionName = body.version;
|
||||
const platform = body.platform;
|
||||
const startup = body.startup;
|
||||
const setup = body.setup ?? "";
|
||||
const delta = body.delta ?? false;
|
||||
const umuId = body.umuId;
|
||||
|
||||
// startup & delta require more complex checking logic
|
||||
if (!gameId || !versionName || !platform)
|
||||
const platform = body.platform as string | undefined;
|
||||
const launch = (body.launch ?? "") as string;
|
||||
const launchArgs = (body.launchArgs ?? "") as string;
|
||||
const setup = (body.setup ?? "") as string;
|
||||
const setupArgs = (body.setupArgs ?? "") as string;
|
||||
const onlySetup = body.onlySetup ?? (false as boolean);
|
||||
const delta = (body.delta ?? false) as boolean;
|
||||
const umuId = (body.umuId ?? "") as string;
|
||||
|
||||
if (!gameId || !versionName)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
"ID, version, platform, setup, and startup (if not in update mode) are required.",
|
||||
statusMessage: "Game ID and version are required.",
|
||||
});
|
||||
|
||||
if (umuId && typeof umuId !== "string")
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "If specified, UMU ID must be a string.",
|
||||
});
|
||||
if (!platform)
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing platform." });
|
||||
|
||||
if (!delta && !startup)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Startup executable is required for non-update versions",
|
||||
});
|
||||
const platformParsed = parsePlatform(platform);
|
||||
if (!platformParsed)
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid platform." });
|
||||
|
||||
if (delta) {
|
||||
const validOverlayVersions = await prisma.gameVersion.count({
|
||||
where: { gameId: gameId, platform: platform, delta: false },
|
||||
where: { gameId: gameId, platform: platformParsed, delta: false },
|
||||
});
|
||||
if (validOverlayVersions == 0)
|
||||
throw createError({
|
||||
@@ -46,17 +44,39 @@ export default defineEventHandler(async (h3) => {
|
||||
});
|
||||
}
|
||||
|
||||
const taskId = await libraryManager.importVersion(
|
||||
gameId,
|
||||
versionName,
|
||||
{
|
||||
platform,
|
||||
startup,
|
||||
setup,
|
||||
umuId,
|
||||
},
|
||||
delta
|
||||
);
|
||||
if (umuId && typeof umuId !== "string")
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "If specified, UMU ID must be a string.",
|
||||
});
|
||||
|
||||
if (onlySetup) {
|
||||
if (!setup)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Setup required in "setup mode".',
|
||||
});
|
||||
} else {
|
||||
if (!delta && !launch)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Startup executable is required for non-update versions",
|
||||
});
|
||||
}
|
||||
|
||||
// startup & delta require more complex checking logic
|
||||
const taskId = await libraryManager.importVersion(gameId, versionName, {
|
||||
platform,
|
||||
onlySetup,
|
||||
|
||||
launch,
|
||||
launchArgs,
|
||||
setup,
|
||||
setupArgs,
|
||||
|
||||
umuId,
|
||||
delta,
|
||||
});
|
||||
if (!taskId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
|
||||
10
server/api/v1/admin/user/index.get.ts
Normal file
10
server/api/v1/admin/user/index.get.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await h3.context.session.getAdminUser(h3);
|
||||
if (!user) throw createError({ statusCode: 403 });
|
||||
|
||||
const users = await prisma.user.findMany({});
|
||||
|
||||
return users;
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import * as jdenticon from "jdenticon";
|
||||
|
||||
// Only really a simple test, in case people mistype their emails
|
||||
const mailRegex = /^\S+@\S+\.\S+$/g;
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
@@ -41,7 +41,7 @@ export default defineEventHandler(async (h3) => {
|
||||
|
||||
const username = useInvitationOrBodyRequirement(
|
||||
"username",
|
||||
(e) => e.length > 5
|
||||
(e) => e.length >= 5
|
||||
);
|
||||
const email = useInvitationOrBodyRequirement("email", (e) =>
|
||||
mailRegex.test(e)
|
||||
@@ -100,7 +100,7 @@ export default defineEventHandler(async (h3) => {
|
||||
displayName,
|
||||
email,
|
||||
profilePicture: profilePictureId,
|
||||
admin: true,
|
||||
admin: invitation.isAdmin,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,5 +16,8 @@ export default defineEventHandler(async (h3) => {
|
||||
|
||||
const token = await clientHandler.generateAuthToken(clientId);
|
||||
|
||||
return `drop://handshake/${clientId}/${token}`;
|
||||
return {
|
||||
redirect: `drop://handshake/${clientId}/${token}`,
|
||||
token: `${clientId}/${token}`,
|
||||
};
|
||||
});
|
||||
|
||||
21
server/api/v1/client/game/manifest.get.ts
Normal file
21
server/api/v1/client/game/manifest.get.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import manifestGenerator from "~/server/internal/downloads/manifest";
|
||||
|
||||
export default defineClientEventHandler(async (h3) => {
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
const version = query.version?.toString();
|
||||
if (!id || !version)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Missing id or version in query",
|
||||
});
|
||||
|
||||
const manifest = await manifestGenerator.generateManifest(id, version);
|
||||
if (!manifest)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid game or version, or no versions added.",
|
||||
});
|
||||
return manifest;
|
||||
});
|
||||
30
server/api/v1/client/game/version.get.ts
Normal file
30
server/api/v1/client/game/version.get.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineClientEventHandler(async (h3) => {
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
const version = query.version?.toString();
|
||||
if (!id || !version)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Missing id or version in query",
|
||||
});
|
||||
|
||||
const gameVersion = await prisma.gameVersion.findUnique({
|
||||
where: {
|
||||
gameId_versionName: {
|
||||
gameId: id,
|
||||
versionName: version,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!gameVersion)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Game version not found",
|
||||
});
|
||||
|
||||
return gameVersion;
|
||||
});
|
||||
46
server/api/v1/client/game/versions.get.ts
Normal file
46
server/api/v1/client/game/versions.get.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { DropManifest } from "~/server/internal/downloads/manifest";
|
||||
|
||||
export default defineClientEventHandler(async (h3, {}) => {
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "No ID in request query",
|
||||
});
|
||||
|
||||
const versions = await prisma.gameVersion.findMany({
|
||||
where: {
|
||||
gameId: id,
|
||||
},
|
||||
orderBy: {
|
||||
versionIndex: "desc", // Latest one first
|
||||
},
|
||||
});
|
||||
|
||||
const mappedVersions = versions
|
||||
.map((version) => {
|
||||
if (!version.dropletManifest) return undefined;
|
||||
const manifest = JSON.parse(
|
||||
version.dropletManifest.toString()
|
||||
) as DropManifest;
|
||||
|
||||
/*
|
||||
TODO: size estimates
|
||||
They are a little complicated because of delta versions
|
||||
Manifests need to be generated with the manifest generator and then
|
||||
added up. I'm a little busy right now to implement this, though.
|
||||
*/
|
||||
|
||||
const newVersion = { ...version, dropletManifest: undefined };
|
||||
delete newVersion.dropletManifest;
|
||||
return {
|
||||
...newVersion,
|
||||
};
|
||||
})
|
||||
.filter((e) => e);
|
||||
|
||||
return mappedVersions;
|
||||
});
|
||||
@@ -15,15 +15,6 @@ export default defineClientEventHandler(async (h3, {}) => {
|
||||
where: {
|
||||
gameId: id,
|
||||
},
|
||||
select: {
|
||||
versionIndex: true,
|
||||
versionName: true,
|
||||
platform: true,
|
||||
setupCommand: true,
|
||||
launchCommand: true,
|
||||
delta: true,
|
||||
dropletManifest: true,
|
||||
},
|
||||
orderBy: {
|
||||
versionIndex: "desc", // Latest one first
|
||||
},
|
||||
|
||||
26
server/api/v1/collection/[id]/entry.delete.ts
Normal file
26
server/api/v1/collection/[id]/entry.delete.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const id = getRouterParam(h3, "id");
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "ID required in route params",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const gameId = body.id;
|
||||
if (!gameId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
|
||||
|
||||
await userLibraryManager.collectionRemove(id, gameId);
|
||||
return {};
|
||||
});
|
||||
26
server/api/v1/collection/[id]/entry.post.ts
Normal file
26
server/api/v1/collection/[id]/entry.post.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const id = getRouterParam(h3, "id");
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "ID required in route params",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const gameId = body.id;
|
||||
if (!gameId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
|
||||
|
||||
await userLibraryManager.collectionAdd(id, gameId);
|
||||
return {};
|
||||
});
|
||||
20
server/api/v1/collection/[id]/index.delete.ts
Normal file
20
server/api/v1/collection/[id]/index.delete.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const id = getRouterParam(h3, "id");
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "ID required in route params",
|
||||
});
|
||||
|
||||
const collection = await userLibraryManager.deleteCollection(id);
|
||||
return collection;
|
||||
});
|
||||
20
server/api/v1/collection/[id]/index.get.ts
Normal file
20
server/api/v1/collection/[id]/index.get.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const id = getRouterParam(h3, "id");
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "ID required in route params",
|
||||
});
|
||||
|
||||
const collection = await userLibraryManager.fetchCollection(id);
|
||||
return collection;
|
||||
});
|
||||
19
server/api/v1/collection/default/entry.delete.ts
Normal file
19
server/api/v1/collection/default/entry.delete.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const gameId = body.id;
|
||||
if (!gameId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
|
||||
|
||||
await userLibraryManager.libraryRemove(gameId, userId);
|
||||
return {};
|
||||
});
|
||||
19
server/api/v1/collection/default/entry.post.ts
Normal file
19
server/api/v1/collection/default/entry.post.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const gameId = body.id;
|
||||
if (!gameId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
|
||||
|
||||
await userLibraryManager.libraryRemove(gameId, userId);
|
||||
return {};
|
||||
});
|
||||
13
server/api/v1/collection/index.get.ts
Normal file
13
server/api/v1/collection/index.get.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const collections = await userLibraryManager.fetchCollections(userId);
|
||||
return collections;
|
||||
});
|
||||
19
server/api/v1/collection/index.post.ts
Normal file
19
server/api/v1/collection/index.post.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const name = body.name;
|
||||
if (!name)
|
||||
throw createError({ statusCode: 400, statusMessage: "Requires name" });
|
||||
|
||||
const collections = await userLibraryManager.fetchCollections(userId);
|
||||
return collections;
|
||||
});
|
||||
@@ -25,16 +25,7 @@ export default defineWebSocketHandler({
|
||||
|
||||
const admin = session.getAdminUser(token);
|
||||
adminSocketSessions[peer.id] = admin !== undefined;
|
||||
|
||||
const rtMsg: TaskMessage = {
|
||||
id: "connect",
|
||||
name: "Connect",
|
||||
success: true,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
};
|
||||
peer.send(JSON.stringify(rtMsg));
|
||||
peer.send(`connect`);
|
||||
},
|
||||
message(peer, message) {
|
||||
if (!peer.id) return;
|
||||
|
||||
11
server/internal/applibrary/README.md
Normal file
11
server/internal/applibrary/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Library Format
|
||||
|
||||
Drop uses a filesystem-based library format, as it targets homelabs and not enterprise-grade solutions. The format works as follows:
|
||||
|
||||
## /{game name}
|
||||
|
||||
The game name is only used for initial matching, and doesn't affect actual metadata. Metadata is linked to the game's database entry, which is linked to it's filesystem name (they, however, can be completely different).
|
||||
|
||||
## /{game name}/{version name}
|
||||
|
||||
The version name can be anything. Versions have to manually imported within the web UI. There, you can change the order of the updates and mark them as deltas. Delta updates apply files over the previous versions.
|
||||
309
server/internal/applibrary/index.ts
Normal file
309
server/internal/applibrary/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* The Library Manager keeps track of games in Drop's library and their various states.
|
||||
* It uses path relative to the library, so it can moved without issue
|
||||
*
|
||||
* It also provides the endpoints with information about unmatched games
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import prisma from "../db/database";
|
||||
import { GameVersion, Platform } from "@prisma/client";
|
||||
import { fuzzy } from "fast-fuzzy";
|
||||
import { recursivelyReaddir } from "../utils/recursivedirs";
|
||||
import taskHandler from "../tasks";
|
||||
import { parsePlatform } from "../utils/parseplatform";
|
||||
import droplet from "@drop/droplet";
|
||||
|
||||
class AppLibraryManager {
|
||||
private basePath: string;
|
||||
|
||||
constructor() {
|
||||
this.basePath = process.env.LIBRARY ?? "./.data/library";
|
||||
fs.mkdirSync(this.basePath, { recursive: true });
|
||||
}
|
||||
|
||||
fetchLibraryPath() {
|
||||
return this.basePath;
|
||||
}
|
||||
|
||||
async fetchAllUnimportedGames() {
|
||||
const dirs = fs.readdirSync(this.basePath).filter((e) => {
|
||||
const fullDir = path.join(this.basePath, e);
|
||||
return fs.lstatSync(fullDir).isDirectory();
|
||||
});
|
||||
|
||||
const validGames = await prisma.game.findMany({
|
||||
where: {
|
||||
libraryBasePath: { in: dirs },
|
||||
},
|
||||
select: {
|
||||
libraryBasePath: true,
|
||||
},
|
||||
});
|
||||
const validGameDirs = validGames.map((e) => e.libraryBasePath);
|
||||
|
||||
const unregisteredGames = dirs.filter((e) => !validGameDirs.includes(e));
|
||||
|
||||
return unregisteredGames;
|
||||
}
|
||||
|
||||
async fetchUnimportedGameVersions(
|
||||
libraryBasePath: string,
|
||||
versions: Array<GameVersion>
|
||||
) {
|
||||
const gameDir = path.join(this.basePath, libraryBasePath);
|
||||
const versionsDirs = fs.readdirSync(gameDir);
|
||||
const importedVersionDirs = versions.map((e) => e.versionName);
|
||||
const unimportedVersions = versionsDirs.filter(
|
||||
(e) => !importedVersionDirs.includes(e)
|
||||
);
|
||||
|
||||
return unimportedVersions;
|
||||
}
|
||||
|
||||
async fetchGamesWithStatus() {
|
||||
const games = await prisma.game.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
versions: true,
|
||||
mName: true,
|
||||
mShortDescription: true,
|
||||
metadataSource: true,
|
||||
mDevelopers: true,
|
||||
mPublishers: true,
|
||||
mIconId: true,
|
||||
libraryBasePath: true,
|
||||
},
|
||||
orderBy: {
|
||||
mName: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return await Promise.all(
|
||||
games.map(async (e) => ({
|
||||
game: e,
|
||||
status: {
|
||||
noVersions: e.versions.length == 0,
|
||||
unimportedVersions: await this.fetchUnimportedGameVersions(
|
||||
e.libraryBasePath,
|
||||
e.versions
|
||||
),
|
||||
},
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async fetchUnimportedVersions(gameId: string) {
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: {
|
||||
versions: {
|
||||
select: {
|
||||
versionName: true,
|
||||
},
|
||||
},
|
||||
libraryBasePath: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!game) return undefined;
|
||||
const targetDir = path.join(this.basePath, game.libraryBasePath);
|
||||
if (!fs.existsSync(targetDir))
|
||||
throw new Error(
|
||||
"Game in database, but no physical directory? Something is very very wrong..."
|
||||
);
|
||||
const versions = fs.readdirSync(targetDir);
|
||||
const validVersions = versions.filter((versionDir) => {
|
||||
const versionPath = path.join(targetDir, versionDir);
|
||||
const stat = fs.statSync(versionPath);
|
||||
return stat.isDirectory();
|
||||
});
|
||||
const currentVersions = game.versions.map((e) => e.versionName);
|
||||
|
||||
const unimportedVersions = validVersions.filter(
|
||||
(e) => !currentVersions.includes(e)
|
||||
);
|
||||
return unimportedVersions;
|
||||
}
|
||||
|
||||
async fetchUnimportedVersionInformation(gameId: string, versionName: string) {
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: { libraryBasePath: true, mName: true },
|
||||
});
|
||||
if (!game) return undefined;
|
||||
const targetDir = path.join(
|
||||
this.basePath,
|
||||
game.libraryBasePath,
|
||||
versionName
|
||||
);
|
||||
if (!fs.existsSync(targetDir)) return undefined;
|
||||
|
||||
const fileExts: { [key: string]: string[] } = {
|
||||
Linux: [
|
||||
// Ext for Unity games
|
||||
".x86_64",
|
||||
// Shell scripts
|
||||
".sh",
|
||||
// No extension is common for Linux binaries
|
||||
"",
|
||||
],
|
||||
Windows: [
|
||||
// Pretty much the only one
|
||||
".exe",
|
||||
],
|
||||
};
|
||||
|
||||
const options: Array<{
|
||||
filename: string;
|
||||
platform: string;
|
||||
match: number;
|
||||
}> = [];
|
||||
|
||||
const files = recursivelyReaddir(targetDir, 2);
|
||||
for (const file of files) {
|
||||
const filename = path.basename(file);
|
||||
const dotLocation = file.lastIndexOf(".");
|
||||
const ext = dotLocation == -1 ? "" : file.slice(dotLocation);
|
||||
for (const [platform, checkExts] of Object.entries(fileExts)) {
|
||||
for (const checkExt of checkExts) {
|
||||
if (checkExt != ext) continue;
|
||||
const fuzzyValue = fuzzy(filename, game.mName);
|
||||
const relative = path.relative(targetDir, file);
|
||||
options.push({
|
||||
filename: relative,
|
||||
platform: platform,
|
||||
match: fuzzyValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedOptions = options.sort((a, b) => b.match - a.match);
|
||||
|
||||
return sortedOptions;
|
||||
}
|
||||
|
||||
// Checks are done in least to most expensive order
|
||||
async checkUnimportedGamePath(targetPath: string) {
|
||||
const targetDir = path.join(this.basePath, targetPath);
|
||||
if (!fs.existsSync(targetDir)) return false;
|
||||
|
||||
const hasGame =
|
||||
(await prisma.game.count({ where: { libraryBasePath: targetPath } })) > 0;
|
||||
if (hasGame) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async importVersion(
|
||||
gameId: string,
|
||||
versionName: string,
|
||||
metadata: {
|
||||
platform: string;
|
||||
onlySetup: boolean;
|
||||
|
||||
setup: string;
|
||||
setupArgs: string;
|
||||
launch: string;
|
||||
launchArgs: string;
|
||||
delta: boolean;
|
||||
|
||||
umuId: string;
|
||||
}
|
||||
) {
|
||||
const taskId = `import:${gameId}:${versionName}`;
|
||||
|
||||
const platform = parsePlatform(metadata.platform);
|
||||
if (!platform) return undefined;
|
||||
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: { mName: true, libraryBasePath: true },
|
||||
});
|
||||
if (!game) return undefined;
|
||||
|
||||
const baseDir = path.join(this.basePath, game.libraryBasePath, versionName);
|
||||
if (!fs.existsSync(baseDir)) return undefined;
|
||||
|
||||
taskHandler.create({
|
||||
id: taskId,
|
||||
name: `Importing version ${versionName} for ${game.mName}`,
|
||||
requireAdmin: true,
|
||||
async run({ progress, log }) {
|
||||
// First, create the manifest via droplet.
|
||||
// This takes up 90% of our progress, so we wrap it in a *0.9
|
||||
const manifest = await new Promise<string>((resolve, reject) => {
|
||||
droplet.generateManifest(
|
||||
baseDir,
|
||||
(err, value) => {
|
||||
if (err) return reject(err);
|
||||
progress(value * 0.9);
|
||||
},
|
||||
(err, line) => {
|
||||
if (err) return reject(err);
|
||||
log(line);
|
||||
},
|
||||
(err, manifest) => {
|
||||
if (err) return reject(err);
|
||||
resolve(manifest);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
log("Created manifest successfully!");
|
||||
|
||||
const currentIndex = await prisma.gameVersion.count({
|
||||
where: { gameId: gameId },
|
||||
});
|
||||
|
||||
// Then, create the database object
|
||||
if (metadata.onlySetup) {
|
||||
await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
dropletManifest: manifest,
|
||||
versionIndex: currentIndex,
|
||||
delta: metadata.delta,
|
||||
umuIdOverride: metadata.umuId,
|
||||
platform: platform,
|
||||
|
||||
onlySetup: true,
|
||||
setupCommand: metadata.setup,
|
||||
setupArgs: metadata.setupArgs.split(" "),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
dropletManifest: manifest,
|
||||
versionIndex: currentIndex,
|
||||
delta: metadata.delta,
|
||||
umuIdOverride: metadata.umuId,
|
||||
platform: platform,
|
||||
|
||||
onlySetup: false,
|
||||
setupCommand: metadata.setup,
|
||||
setupArgs: metadata.setupArgs.split(" "),
|
||||
launchCommand: metadata.launch,
|
||||
launchArgs: metadata.launchArgs.split(" "),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
log("Successfully created version!");
|
||||
|
||||
progress(100);
|
||||
},
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
|
||||
export const appLibraryManager = new AppLibraryManager();
|
||||
export default appLibraryManager;
|
||||
@@ -144,6 +144,8 @@ class LibraryManager {
|
||||
Linux: [
|
||||
// Ext for Unity games
|
||||
".x86_64",
|
||||
// Shell scripts
|
||||
".sh",
|
||||
// No extension is common for Linux binaries
|
||||
"",
|
||||
],
|
||||
@@ -168,8 +170,9 @@ class LibraryManager {
|
||||
for (const checkExt of checkExts) {
|
||||
if (checkExt != ext) continue;
|
||||
const fuzzyValue = fuzzy(filename, game.mName);
|
||||
const relative = path.relative(targetDir, file);
|
||||
options.push({
|
||||
filename: file,
|
||||
filename: relative,
|
||||
platform: platform,
|
||||
match: fuzzyValue,
|
||||
});
|
||||
@@ -178,19 +181,8 @@ class LibraryManager {
|
||||
}
|
||||
|
||||
const sortedOptions = options.sort((a, b) => b.match - a.match);
|
||||
let startupGuess = "";
|
||||
let platformGuess = "";
|
||||
if (sortedOptions.length > 0) {
|
||||
const finalChoice = sortedOptions[0];
|
||||
const finalChoiceRelativePath = path.relative(
|
||||
targetDir,
|
||||
finalChoice.filename
|
||||
);
|
||||
startupGuess = finalChoiceRelativePath;
|
||||
platformGuess = finalChoice.platform;
|
||||
}
|
||||
|
||||
return { startupGuess, platformGuess };
|
||||
return sortedOptions;
|
||||
}
|
||||
|
||||
// Checks are done in least to most expensive order
|
||||
@@ -210,11 +202,16 @@ class LibraryManager {
|
||||
versionName: string,
|
||||
metadata: {
|
||||
platform: string;
|
||||
onlySetup: boolean;
|
||||
|
||||
setup: string;
|
||||
startup: string;
|
||||
umuId: string | undefined;
|
||||
},
|
||||
delta = false
|
||||
setupArgs: string;
|
||||
launch: string;
|
||||
launchArgs: string;
|
||||
delta: boolean;
|
||||
|
||||
umuId: string;
|
||||
}
|
||||
) {
|
||||
const taskId = `import:${gameId}:${versionName}`;
|
||||
|
||||
@@ -262,19 +259,41 @@ class LibraryManager {
|
||||
});
|
||||
|
||||
// Then, create the database object
|
||||
const version = await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
platform: platform,
|
||||
setupCommand: metadata.setup,
|
||||
launchCommand: metadata.startup,
|
||||
umuIdOverride: metadata.umuId,
|
||||
dropletManifest: manifest,
|
||||
versionIndex: currentIndex,
|
||||
delta: delta,
|
||||
},
|
||||
});
|
||||
if (metadata.onlySetup) {
|
||||
await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
dropletManifest: manifest,
|
||||
versionIndex: currentIndex,
|
||||
delta: metadata.delta,
|
||||
umuIdOverride: metadata.umuId,
|
||||
platform: platform,
|
||||
|
||||
onlySetup: true,
|
||||
setupCommand: metadata.setup,
|
||||
setupArgs: metadata.setupArgs.split(" "),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
dropletManifest: manifest,
|
||||
versionIndex: currentIndex,
|
||||
delta: metadata.delta,
|
||||
umuIdOverride: metadata.umuId,
|
||||
platform: platform,
|
||||
|
||||
onlySetup: false,
|
||||
setupCommand: metadata.setup,
|
||||
setupArgs: metadata.setupArgs.split(" "),
|
||||
launchCommand: metadata.launch,
|
||||
launchArgs: metadata.launchArgs.split(" "),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
log("Successfully created version!");
|
||||
|
||||
|
||||
@@ -50,15 +50,19 @@ export class MetadataHandler {
|
||||
const queryTransformationPromise = new Promise<
|
||||
InternalGameMetadataResult[]
|
||||
>(async (resolve, reject) => {
|
||||
const results = await provider.search(query);
|
||||
const mappedResults: InternalGameMetadataResult[] = results.map(
|
||||
(result) =>
|
||||
Object.assign({}, result, {
|
||||
sourceId: provider.id(),
|
||||
sourceName: provider.name(),
|
||||
})
|
||||
);
|
||||
resolve(mappedResults);
|
||||
try {
|
||||
const results = await provider.search(query);
|
||||
const mappedResults: InternalGameMetadataResult[] = results.map(
|
||||
(result) =>
|
||||
Object.assign({}, result, {
|
||||
sourceId: provider.id(),
|
||||
sourceName: provider.name(),
|
||||
})
|
||||
);
|
||||
resolve(mappedResults);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
promises.push(queryTransformationPromise);
|
||||
}
|
||||
@@ -72,6 +76,21 @@ export class MetadataHandler {
|
||||
return successfulResults;
|
||||
}
|
||||
|
||||
async createGameWithoutMetadata(libraryBasePath: string) {
|
||||
return await this.createGame(
|
||||
{
|
||||
id: "",
|
||||
name: libraryBasePath,
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 0,
|
||||
sourceId: "manual",
|
||||
sourceName: "Manual",
|
||||
},
|
||||
libraryBasePath
|
||||
);
|
||||
}
|
||||
|
||||
async createGame(
|
||||
result: InternalGameMetadataResult,
|
||||
libraryBasePath: string
|
||||
@@ -99,6 +118,7 @@ export class MetadataHandler {
|
||||
try {
|
||||
metadata = await provider.fetchGame({
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
// wrap in anonymous functions to keep references to this
|
||||
publisher: (name: string) => this.fetchPublisher(name),
|
||||
developer: (name: string) => this.fetchDeveloper(name),
|
||||
|
||||
63
server/internal/metadata/manual.ts
Normal file
63
server/internal/metadata/manual.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { MetadataSource } from "@prisma/client";
|
||||
import { MetadataProvider } from ".";
|
||||
import {
|
||||
GameMetadataSearchResult,
|
||||
_FetchGameMetadataParams,
|
||||
GameMetadata,
|
||||
_FetchPublisherMetadataParams,
|
||||
PublisherMetadata,
|
||||
_FetchDeveloperMetadataParams,
|
||||
DeveloperMetadata,
|
||||
} from "./types";
|
||||
import * as jdenticon from "jdenticon";
|
||||
|
||||
export class ManualMetadataProvider implements MetadataProvider {
|
||||
id() {
|
||||
return "manual";
|
||||
}
|
||||
name() {
|
||||
return "Manual";
|
||||
}
|
||||
source() {
|
||||
return MetadataSource.Manual;
|
||||
}
|
||||
async search(query: string) {
|
||||
return [];
|
||||
}
|
||||
async fetchGame({
|
||||
name,
|
||||
publisher,
|
||||
developer,
|
||||
createObject,
|
||||
}: _FetchGameMetadataParams): Promise<GameMetadata> {
|
||||
const icon = jdenticon.toPng(name, 512);
|
||||
const iconId = createObject(icon);
|
||||
|
||||
return {
|
||||
id: "manual",
|
||||
name,
|
||||
shortDescription: "Default description.",
|
||||
description: "# Default description.",
|
||||
released: new Date(),
|
||||
publishers: [],
|
||||
developers: [],
|
||||
reviewCount: 0,
|
||||
reviewRating: 0,
|
||||
|
||||
icon: iconId,
|
||||
coverId: iconId,
|
||||
bannerId: iconId,
|
||||
images: [iconId],
|
||||
};
|
||||
}
|
||||
async fetchPublisher(
|
||||
params: _FetchPublisherMetadataParams
|
||||
): Promise<PublisherMetadata> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async fetchDeveloper(
|
||||
params: _FetchDeveloperMetadataParams
|
||||
): Promise<DeveloperMetadata> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
6
server/internal/metadata/types.d.ts
vendored
6
server/internal/metadata/types.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import { Developer, Publisher } from "@prisma/client";
|
||||
import { ObjectReference } from "../objects";
|
||||
import { ObjectTransactionalHandler, TransactionDataType } from "../objects/transactional";
|
||||
|
||||
export interface GameMetadataSearchResult {
|
||||
id: string;
|
||||
@@ -54,16 +55,17 @@ export type DeveloperMetadata = PublisherMetadata;
|
||||
|
||||
export interface _FetchGameMetadataParams {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
publisher: (query: string) => Promise<Publisher>;
|
||||
developer: (query: string) => Promise<Developer>;
|
||||
|
||||
createObject: (url: string) => ObjectReference;
|
||||
createObject: (data: TransactionDataType) => ObjectReference;
|
||||
}
|
||||
|
||||
export interface _FetchPublisherMetadataParams {
|
||||
query: string;
|
||||
createObject: (url: string) => ObjectReference;
|
||||
createObject: (data: TransactionDataType) => ObjectReference;
|
||||
}
|
||||
|
||||
export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Readable } from "stream";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { objectHandler } from "~/server/plugins/objects";
|
||||
|
||||
type TransactionDataType = string | Readable | Buffer;
|
||||
export type TransactionDataType = string | Readable | Buffer;
|
||||
type TransactionTable = { [key: string]: TransactionDataType }; // ID to data
|
||||
type GlobalTransactionRecord = { [key: string]: TransactionTable }; // Transaction ID to table
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ type TaskRegistryEntry = {
|
||||
success: boolean;
|
||||
progress: number;
|
||||
log: string[];
|
||||
error: string | undefined;
|
||||
error: { title: string; description: string } | undefined;
|
||||
clients: { [key: string]: boolean };
|
||||
name: string;
|
||||
requireAdmin: boolean;
|
||||
@@ -25,27 +25,43 @@ class TaskHandler {
|
||||
|
||||
create(task: Task) {
|
||||
let updateCollectTimeout: NodeJS.Timeout | undefined;
|
||||
let updateCollectResolves: Array<(value: unknown) => void> = [];
|
||||
let logOffset: number = 0;
|
||||
|
||||
const updateAllClients = () => {
|
||||
if (updateCollectTimeout) return;
|
||||
updateCollectTimeout = setTimeout(() => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
const taskMessage: TaskMessage = {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
success: taskEntry.success,
|
||||
progress: taskEntry.progress,
|
||||
error: taskEntry.error,
|
||||
log: taskEntry.log.reverse().slice(0, 50),
|
||||
};
|
||||
for (const client of Object.keys(taskEntry.clients)) {
|
||||
if (!this.clientRegistry[client]) continue;
|
||||
this.clientRegistry[client].send(JSON.stringify(taskMessage));
|
||||
const updateAllClients = (reset = false) =>
|
||||
new Promise((r) => {
|
||||
if (updateCollectTimeout) {
|
||||
updateCollectResolves.push(r);
|
||||
return;
|
||||
}
|
||||
updateCollectTimeout = undefined;
|
||||
}, 100);
|
||||
};
|
||||
updateCollectTimeout = setTimeout(() => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
|
||||
const taskMessage: TaskMessage = {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
success: taskEntry.success,
|
||||
progress: taskEntry.progress,
|
||||
error: taskEntry.error,
|
||||
log: taskEntry.log.slice(logOffset),
|
||||
reset,
|
||||
};
|
||||
logOffset = taskEntry.log.length;
|
||||
|
||||
for (const client of Object.keys(taskEntry.clients)) {
|
||||
if (!this.clientRegistry[client]) continue;
|
||||
this.clientRegistry[client].send(JSON.stringify(taskMessage));
|
||||
}
|
||||
updateCollectTimeout = undefined;
|
||||
|
||||
for (const resolve of updateCollectResolves) {
|
||||
resolve(undefined);
|
||||
}
|
||||
r(undefined);
|
||||
updateCollectResolves = [];
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const progress = (progress: number) => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
@@ -71,29 +87,48 @@ class TaskHandler {
|
||||
requireAdmin: task.requireAdmin ?? false,
|
||||
};
|
||||
|
||||
updateAllClients(true);
|
||||
|
||||
droplet.callAltThreadFunc(async () => {
|
||||
const promiseRun = task.run({ progress, log });
|
||||
promiseRun.then(() => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) throw new Error("No task entry");
|
||||
|
||||
try {
|
||||
await task.run({ progress, log });
|
||||
this.taskRegistry[task.id].success = true;
|
||||
updateAllClients();
|
||||
});
|
||||
promiseRun.catch((error) => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
} catch (error: unknown) {
|
||||
this.taskRegistry[task.id].success = false;
|
||||
this.taskRegistry[task.id].error = error;
|
||||
updateAllClients();
|
||||
});
|
||||
this.taskRegistry[task.id].error = {
|
||||
title: "An error occurred",
|
||||
description: (error as string).toString(),
|
||||
};
|
||||
}
|
||||
await updateAllClients();
|
||||
|
||||
for (const client of Object.keys(taskEntry.clients)) {
|
||||
if (!this.clientRegistry[client]) continue;
|
||||
this.disconnect(client, task.id);
|
||||
}
|
||||
delete this.taskRegistry[task.id];
|
||||
});
|
||||
}
|
||||
|
||||
connect(id: string, taskId: string, peer: PeerImpl, isAdmin = false) {
|
||||
const task = this.taskRegistry[taskId];
|
||||
if (!task) return "Invalid task";
|
||||
if (!task) {
|
||||
peer.send(
|
||||
`error/${taskId}/Unknown task/Drop couldn't find the task you're looking for.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.requireAdmin && !isAdmin) return "Requires admin";
|
||||
if (task.requireAdmin && !isAdmin) {
|
||||
console.warn("user is not an admin, so cannot view this task");
|
||||
peer.send(
|
||||
`error/${taskId}/Unknown task/Drop couldn't find the task you're looking for.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clientRegistry[id] = peer;
|
||||
this.taskRegistry[taskId].clients[id] = true; // Uniquely insert client to avoid sending duplicate traffic
|
||||
@@ -107,15 +142,20 @@ class TaskHandler {
|
||||
progress: task.progress,
|
||||
};
|
||||
peer.send(JSON.stringify(catchupMessage));
|
||||
}
|
||||
|
||||
return true;
|
||||
sendDisconnectEvent(id: string, taskId: string) {
|
||||
const client = this.clientRegistry[id];
|
||||
if (!client) return;
|
||||
client.send(`disconnect/${taskId}`);
|
||||
}
|
||||
|
||||
disconnectAll(id: string) {
|
||||
for (const [taskId] of Object.keys(this.taskRegistry)) {
|
||||
for (const taskId of Object.keys(this.taskRegistry)) {
|
||||
delete this.taskRegistry[taskId].clients[id];
|
||||
this.sendDisconnectEvent(id, taskId);
|
||||
}
|
||||
|
||||
|
||||
delete this.clientRegistry[id];
|
||||
}
|
||||
|
||||
@@ -123,6 +163,7 @@ class TaskHandler {
|
||||
if (!this.taskRegistry[taskId]) return false;
|
||||
|
||||
delete this.taskRegistry[taskId].clients[id];
|
||||
this.sendDisconnectEvent(id, taskId);
|
||||
|
||||
const allClientIds = Object.values(this.taskRegistry)
|
||||
.map((_) => Object.keys(_.clients))
|
||||
@@ -153,8 +194,9 @@ export type TaskMessage = {
|
||||
name: string;
|
||||
success: boolean;
|
||||
progress: number;
|
||||
error: undefined | string;
|
||||
error: undefined | { title: string; description: string };
|
||||
log: string[];
|
||||
reset?: boolean;
|
||||
};
|
||||
|
||||
export type PeerImpl = {
|
||||
|
||||
119
server/internal/userlibrary/index.ts
Normal file
119
server/internal/userlibrary/index.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Handles managing collections
|
||||
*/
|
||||
|
||||
import prisma from "../db/database";
|
||||
|
||||
class UserLibraryManager {
|
||||
// Caches the user's core library
|
||||
private userCoreLibraryCache: { [key: string]: string } = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
private async fetchUserLibrary(userId: string) {
|
||||
if (this.userCoreLibraryCache[userId])
|
||||
return this.userCoreLibraryCache[userId];
|
||||
|
||||
let collection = await prisma.collection.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!collection)
|
||||
collection = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Library",
|
||||
userId,
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.userCoreLibraryCache[userId] = collection.id;
|
||||
|
||||
return collection.id;
|
||||
}
|
||||
|
||||
async libraryAdd(gameId: string, userId: string) {
|
||||
const userLibraryId = await this.fetchUserLibrary(userId);
|
||||
await this.collectionAdd(gameId, userLibraryId);
|
||||
}
|
||||
|
||||
async libraryRemove(gameId: string, userId: string) {
|
||||
const userLibraryId = await this.fetchUserLibrary(userId);
|
||||
await this.collectionRemove(gameId, userLibraryId);
|
||||
}
|
||||
|
||||
async fetchLibrary(userId: string) {
|
||||
const userLibraryId = await this.fetchUserLibrary(userId);
|
||||
const userLibrary = await prisma.collection.findUnique({
|
||||
where: { id: userLibraryId },
|
||||
include: { entries: { include: { game: true } } },
|
||||
});
|
||||
if (!userLibrary) throw new Error("Failed to load user library");
|
||||
return userLibrary;
|
||||
}
|
||||
|
||||
async fetchCollection(collectionId: string) {
|
||||
return await prisma.collection.findUnique({
|
||||
where: { id: collectionId },
|
||||
include: { entries: { include: { game: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
async fetchCollections(userId: string) {
|
||||
await this.fetchUserLibrary(userId); // Ensures user library exists, doesn't have much performance impact due to caching
|
||||
return await prisma.collection.findMany({ where: { userId } });
|
||||
}
|
||||
|
||||
async collectionAdd(gameId: string, collectionId: string) {
|
||||
await prisma.collectionEntry.upsert({
|
||||
where: {
|
||||
collectionId_gameId: {
|
||||
collectionId,
|
||||
gameId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
collectionId,
|
||||
gameId,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
async collectionRemove(gameId: string, collectionId: string) {
|
||||
// Delete if exists
|
||||
return (
|
||||
(
|
||||
await prisma.collectionEntry.deleteMany({
|
||||
where: {
|
||||
collectionId,
|
||||
gameId,
|
||||
},
|
||||
})
|
||||
).count > 0
|
||||
);
|
||||
}
|
||||
|
||||
async collectionCreate(name: string, userId: string) {
|
||||
return await prisma.collection.create({
|
||||
data: {
|
||||
name,
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCollection(collectionId: string) {
|
||||
await prisma.collection.delete({
|
||||
where: {
|
||||
id: collectionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const userLibraryManager = new UserLibraryManager();
|
||||
export default userLibraryManager;
|
||||
@@ -10,8 +10,7 @@ export function recursivelyReaddir(dir: string, depth: number = 100) {
|
||||
const stat = fs.lstatSync(targetDir);
|
||||
if (stat.isDirectory()) {
|
||||
const subdirs = recursivelyReaddir(targetDir, depth - 1);
|
||||
const subdirsWithBase = subdirs.map((e) => path.join(dir, e));
|
||||
result.push(...subdirsWithBase);
|
||||
result.push(...subdirs);
|
||||
continue;
|
||||
}
|
||||
result.push(targetDir);
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { MetadataHandler, MetadataProvider } from "../internal/metadata";
|
||||
import { GiantBombProvider } from "../internal/metadata/giantbomb";
|
||||
import { ManualMetadataProvider } from "../internal/metadata/manual";
|
||||
|
||||
export const metadataHandler = new MetadataHandler();
|
||||
|
||||
const providerCreators: Array<() => MetadataProvider> = [() => new GiantBombProvider()];
|
||||
const providerCreators: Array<() => MetadataProvider> = [
|
||||
() => new GiantBombProvider(),
|
||||
() => new ManualMetadataProvider(),
|
||||
];
|
||||
|
||||
export default defineNitroPlugin(async (nitro) => {
|
||||
for (const creator of providerCreators) {
|
||||
try {
|
||||
const instance = creator();
|
||||
metadataHandler.addProvider(instance);
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
for (const creator of providerCreators) {
|
||||
try {
|
||||
const instance = creator();
|
||||
metadataHandler.addProvider(instance);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
nitro.hooks.hook('request', (h3) => {
|
||||
h3.context.metadataHandler = metadataHandler;
|
||||
})
|
||||
});
|
||||
nitro.hooks.hook("request", (h3) => {
|
||||
h3.context.metadataHandler = metadataHandler;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,9 +15,10 @@ export default defineNitroPlugin((nitro) => {
|
||||
case 403:
|
||||
const userId = await event.context.session.getUserId(event);
|
||||
if (userId) break;
|
||||
console.log("user is signed out, redirecting");
|
||||
return sendRedirect(
|
||||
event,
|
||||
`/signin?redirect=${encodeURIComponent(event.path)}`,
|
||||
`/signin?redirect=${encodeURIComponent(event.path)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import * as forms from "@tailwindcss/forms";
|
||||
import * as tpyo from "@tailwindcss/typography"; // lol
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
|
||||
453
yarn.lock
453
yarn.lock
@@ -278,6 +278,16 @@
|
||||
dependencies:
|
||||
mime "^3.0.0"
|
||||
|
||||
"@csstools/selector-resolve-nested@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a"
|
||||
integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==
|
||||
|
||||
"@csstools/selector-specificity@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b"
|
||||
integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
|
||||
|
||||
"@drop/droplet-linux-x64-gnu@0.7.0", "@drop/droplet-linux-x64-gnu@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.7.0.tgz#128e37707481cfcbbeb057142164f3e637f13f26"
|
||||
@@ -600,6 +610,17 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@koa/router@^12.0.1":
|
||||
version "12.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@koa/router/-/router-12.0.2.tgz#286d51959ed611255faa944818a112e35567835a"
|
||||
integrity sha512-sYcHglGKTxGF+hQ6x67xDfkE9o+NhVlRHBqq6gLywaMc6CojK/5vFZByphdonKinYlMLkEkacm+HEse9HzwgTA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
http-errors "^2.0.0"
|
||||
koa-compose "^4.1.0"
|
||||
methods "^1.1.2"
|
||||
path-to-regexp "^6.3.0"
|
||||
|
||||
"@kwsites/file-exists@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99"
|
||||
@@ -929,6 +950,25 @@
|
||||
unist-util-visit "^5.0.0"
|
||||
unwasm "^0.3.9"
|
||||
|
||||
"@nuxtjs/tailwindcss@^6.12.2":
|
||||
version "6.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@nuxtjs/tailwindcss/-/tailwindcss-6.12.2.tgz#3fa41b0b9361cd69ec14934f800e66225b3c9e1f"
|
||||
integrity sha512-qPJiFH67CkTj/2kBGBzqXihOD1rQXMsbVS4vdQvfBxOBLPfGhU1yw7AATdhPl2BBjO2krjJLuZj39t7dnDYOwg==
|
||||
dependencies:
|
||||
"@nuxt/kit" "^3.13.2"
|
||||
autoprefixer "^10.4.20"
|
||||
consola "^3.2.3"
|
||||
defu "^6.1.4"
|
||||
h3 "^1.13.0"
|
||||
klona "^2.0.6"
|
||||
pathe "^1.1.2"
|
||||
postcss "^8.4.47"
|
||||
postcss-nesting "^13.0.0"
|
||||
tailwind-config-viewer "^2.0.4"
|
||||
tailwindcss "~3.4.13"
|
||||
ufo "^1.5.4"
|
||||
unctx "^2.3.1"
|
||||
|
||||
"@parcel/watcher-android-arm64@2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a"
|
||||
@@ -1770,6 +1810,14 @@ abort-controller@^3.0.0:
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
accepts@^1.3.5:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||
dependencies:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-import-attributes@^1.9.5:
|
||||
version "1.9.5"
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
|
||||
@@ -1911,6 +1959,13 @@ async-sema@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.1.tgz#e527c08758a0f8f6f9f15f799a173ff3c40ea808"
|
||||
integrity sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==
|
||||
|
||||
async@^2.6.4:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.2.4:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
||||
@@ -1921,6 +1976,11 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
autoprefixer@^10.4.20:
|
||||
version "10.4.20"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
|
||||
@@ -2074,6 +2134,14 @@ cac@^6.7.14:
|
||||
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
|
||||
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
|
||||
|
||||
cache-content-type@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
|
||||
integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==
|
||||
dependencies:
|
||||
mime-types "^2.1.18"
|
||||
ylru "^1.2.0"
|
||||
|
||||
camelcase-css@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
||||
@@ -2106,7 +2174,7 @@ ccount@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
chalk@^4.1.1:
|
||||
chalk@^4.1.1, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -2211,6 +2279,11 @@ cluster-key-slot@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
|
||||
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
@@ -2260,6 +2333,11 @@ commander@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@^6.0.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||
|
||||
commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
@@ -2316,6 +2394,18 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
|
||||
|
||||
content-disposition@~0.5.2:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
convert-source-map@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
@@ -2326,6 +2416,14 @@ cookie-es@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.2.2.tgz#18ceef9eb513cac1cb6c14bcbf8bdb2679b34821"
|
||||
integrity sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==
|
||||
|
||||
cookies@~0.9.0:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
|
||||
integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==
|
||||
dependencies:
|
||||
depd "~2.0.0"
|
||||
keygrip "~1.1.0"
|
||||
|
||||
copy-anything@^3.0.2:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
|
||||
@@ -2504,6 +2602,20 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, d
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@^3.1.0, debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.3.2:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decode-named-character-reference@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
|
||||
@@ -2511,6 +2623,11 @@ decode-named-character-reference@^1.0.0:
|
||||
dependencies:
|
||||
character-entities "^2.0.0"
|
||||
|
||||
deep-equal@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
@@ -2559,11 +2676,16 @@ denque@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
|
||||
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
||||
|
||||
depd@2.0.0:
|
||||
depd@2.0.0, depd@^2.0.0, depd@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
|
||||
|
||||
dequal@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
@@ -2574,7 +2696,7 @@ destr@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
|
||||
integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==
|
||||
|
||||
destroy@1.2.0:
|
||||
destroy@1.2.0, destroy@^1.0.4:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
@@ -2703,7 +2825,7 @@ emoticon@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.1.0.tgz#d5a156868ee173095627a33de3f1e914c3dde79e"
|
||||
integrity sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
encodeurl@^1.0.2, encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
@@ -2816,7 +2938,7 @@ escalade@^3.1.1, escalade@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||
|
||||
escape-html@~1.0.3:
|
||||
escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
@@ -3006,7 +3128,7 @@ fraction.js@^4.3.7:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
fresh@0.5.2:
|
||||
fresh@0.5.2, fresh@~0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
@@ -3020,6 +3142,16 @@ fs-extra@^11.1.0, fs-extra@^11.2.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^9.0.1:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
||||
dependencies:
|
||||
at-least-node "^1.0.0"
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-minipass@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||
@@ -3147,7 +3279,7 @@ glob@^10.0.0, glob@^10.3.10:
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^7.1.3:
|
||||
glob@^7.1.3, glob@^7.2.0:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@@ -3224,6 +3356,18 @@ has-flag@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
has-symbols@^1.0.3:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
has-unicode@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
@@ -3365,7 +3509,15 @@ html-void-elements@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
|
||||
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
|
||||
|
||||
http-errors@2.0.0:
|
||||
http-assert@^1.3.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f"
|
||||
integrity sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==
|
||||
dependencies:
|
||||
deep-equal "~1.0.1"
|
||||
http-errors "~1.8.0"
|
||||
|
||||
http-errors@2.0.0, http-errors@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
||||
@@ -3376,6 +3528,27 @@ http-errors@2.0.0:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.8.0:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
||||
integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.2.0"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-errors@~1.6.2:
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
|
||||
integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.3"
|
||||
setprototypeof "1.1.0"
|
||||
statuses ">= 1.4.0 < 2"
|
||||
|
||||
http-shutdown@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f"
|
||||
@@ -3466,6 +3639,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inherits@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
|
||||
|
||||
ini@4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1"
|
||||
@@ -3553,6 +3731,13 @@ is-fullwidth-code-point@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-generator-function@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
|
||||
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
|
||||
dependencies:
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
@@ -3629,7 +3814,7 @@ is-what@^4.1.8:
|
||||
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
|
||||
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
|
||||
|
||||
is-wsl@^2.2.0:
|
||||
is-wsl@^2.1.1, is-wsl@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||
@@ -3737,6 +3922,13 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
keygrip@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
||||
integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
|
||||
dependencies:
|
||||
tsscmp "1.0.6"
|
||||
|
||||
kleur@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||
@@ -3752,6 +3944,65 @@ knitwork@^1.0.0, knitwork@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/knitwork/-/knitwork-1.1.0.tgz#d8c9feafadd7ee744ff64340b216a52c7199c417"
|
||||
integrity sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==
|
||||
|
||||
koa-compose@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
|
||||
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
|
||||
|
||||
koa-convert@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
|
||||
integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
koa-compose "^4.1.0"
|
||||
|
||||
koa-send@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
|
||||
integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
http-errors "^1.7.3"
|
||||
resolve-path "^1.4.0"
|
||||
|
||||
koa-static@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
|
||||
integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
koa-send "^5.0.0"
|
||||
|
||||
koa@^2.14.2:
|
||||
version "2.15.3"
|
||||
resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.3.tgz#062809266ee75ce0c75f6510a005b0e38f8c519a"
|
||||
integrity sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==
|
||||
dependencies:
|
||||
accepts "^1.3.5"
|
||||
cache-content-type "^1.0.0"
|
||||
content-disposition "~0.5.2"
|
||||
content-type "^1.0.4"
|
||||
cookies "~0.9.0"
|
||||
debug "^4.3.2"
|
||||
delegates "^1.0.0"
|
||||
depd "^2.0.0"
|
||||
destroy "^1.0.4"
|
||||
encodeurl "^1.0.2"
|
||||
escape-html "^1.0.3"
|
||||
fresh "~0.5.2"
|
||||
http-assert "^1.3.0"
|
||||
http-errors "^1.6.3"
|
||||
is-generator-function "^1.0.7"
|
||||
koa-compose "^4.1.0"
|
||||
koa-convert "^2.0.0"
|
||||
on-finished "^2.3.0"
|
||||
only "~0.0.2"
|
||||
parseurl "^1.3.2"
|
||||
statuses "^1.5.0"
|
||||
type-is "^1.6.16"
|
||||
vary "^1.1.2"
|
||||
|
||||
kolorist@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
|
||||
@@ -3782,6 +4033,11 @@ lilconfig@^3.0.0, lilconfig@^3.1.2:
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb"
|
||||
integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==
|
||||
|
||||
lilconfig@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
|
||||
integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
@@ -3859,7 +4115,7 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
||||
|
||||
lodash@^4.17.15:
|
||||
lodash@^4.17.14, lodash@^4.17.15:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -4069,6 +4325,11 @@ mdurl@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
@@ -4079,6 +4340,11 @@ merge2@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
methods@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||
|
||||
micromark-core-commonmark@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz#9a45510557d068605c6e9a80f282b2bb8581e43d"
|
||||
@@ -4388,7 +4654,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.35:
|
||||
mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
@@ -4441,6 +4707,11 @@ minimatch@^9.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
minipass@^3.0.0:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
|
||||
@@ -4476,6 +4747,13 @@ mitt@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
|
||||
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||
|
||||
mkdirp@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mkdirp@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
@@ -4511,7 +4789,7 @@ ms@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||
|
||||
ms@2.1.3, ms@^2.1.3:
|
||||
ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
@@ -4545,6 +4823,11 @@ napi-wasm@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.3.tgz#7bb95c88e6561f84880bb67195437b1cfbe99224"
|
||||
integrity sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg==
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
nitropack@^2.10.2, nitropack@^2.9.7:
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/nitropack/-/nitropack-2.10.2.tgz#ddb52dfa0a267ec06baad202394558bd8d354bd3"
|
||||
@@ -4820,7 +5103,7 @@ ohash@^1.1.3, ohash@^1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72"
|
||||
integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==
|
||||
|
||||
on-finished@2.4.1:
|
||||
on-finished@2.4.1, on-finished@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
@@ -4848,6 +5131,11 @@ oniguruma-to-js@0.4.3:
|
||||
dependencies:
|
||||
regex "^4.3.2"
|
||||
|
||||
only@~0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
|
||||
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
|
||||
|
||||
open@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1"
|
||||
@@ -4858,6 +5146,14 @@ open@^10.1.0:
|
||||
is-inside-container "^1.0.0"
|
||||
is-wsl "^3.1.0"
|
||||
|
||||
open@^7.0.4:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
open@^8.4.0:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
|
||||
@@ -4946,12 +5242,12 @@ parse5@^7.0.0, parse5@^7.2.0:
|
||||
dependencies:
|
||||
entities "^4.5.0"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
parseurl@^1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
@@ -4979,6 +5275,11 @@ path-scurry@^1.11.1:
|
||||
lru-cache "^10.2.0"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
|
||||
path-to-regexp@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
|
||||
integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
|
||||
|
||||
path-type@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8"
|
||||
@@ -5038,6 +5339,15 @@ pluralize@^8.0.0:
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||
|
||||
portfinder@^1.0.26:
|
||||
version "1.0.32"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
|
||||
integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
|
||||
dependencies:
|
||||
async "^2.6.4"
|
||||
debug "^3.2.7"
|
||||
mkdirp "^0.5.6"
|
||||
|
||||
postcss-calc@^10.0.2:
|
||||
version "10.0.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-10.0.2.tgz#15f01635a27b9d38913a98c4ef2877f5b715b439"
|
||||
@@ -5168,6 +5478,15 @@ postcss-nested@^6.2.0:
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.1.1"
|
||||
|
||||
postcss-nesting@^13.0.0:
|
||||
version "13.0.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e"
|
||||
integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==
|
||||
dependencies:
|
||||
"@csstools/selector-resolve-nested" "^3.0.0"
|
||||
"@csstools/selector-specificity" "^5.0.0"
|
||||
postcss-selector-parser "^7.0.0"
|
||||
|
||||
postcss-normalize-charset@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz#92244ae73c31bf8f8885d5f16ff69e857ac6c001"
|
||||
@@ -5269,6 +5588,14 @@ postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-selector-parser@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz#41bd8b56f177c093ca49435f65731befe25d6b9c"
|
||||
integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-svgo@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.0.1.tgz#2b63571d8e9568384df334bac9917baff4d23f58"
|
||||
@@ -5589,6 +5916,15 @@ remark-stringify@^11.0.0:
|
||||
mdast-util-to-markdown "^2.0.0"
|
||||
unified "^11.0.0"
|
||||
|
||||
replace-in-file@^6.1.0:
|
||||
version "6.3.5"
|
||||
resolved "https://registry.yarnpkg.com/replace-in-file/-/replace-in-file-6.3.5.tgz#ff956b0ab5bc96613207d603d197cd209400a654"
|
||||
integrity sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
glob "^7.2.0"
|
||||
yargs "^17.2.1"
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
@@ -5604,6 +5940,14 @@ resolve-from@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
|
||||
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
|
||||
|
||||
resolve-path@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
|
||||
integrity sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==
|
||||
dependencies:
|
||||
http-errors "~1.6.2"
|
||||
path-is-absolute "1.0.1"
|
||||
|
||||
resolve@^1.1.7, resolve@^1.22.1, resolve@^1.22.8:
|
||||
version "1.22.8"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
|
||||
@@ -5679,7 +6023,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@@ -5770,6 +6114,11 @@ set-blocking@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||
|
||||
setprototypeof@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
|
||||
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
@@ -5925,6 +6274,11 @@ statuses@2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
|
||||
|
||||
std-env@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
@@ -6122,6 +6476,20 @@ system-architecture@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d"
|
||||
integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==
|
||||
|
||||
tailwind-config-viewer@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-config-viewer/-/tailwind-config-viewer-2.0.4.tgz#5f47ef0f0ba3719557f88628de8bf276cad7a4cb"
|
||||
integrity sha512-icvcmdMmt9dphvas8wL40qttrHwAnW3QEN4ExJ2zICjwRsPj7gowd1cOceaWG3IfTuM/cTNGQcx+bsjMtmV+cw==
|
||||
dependencies:
|
||||
"@koa/router" "^12.0.1"
|
||||
commander "^6.0.0"
|
||||
fs-extra "^9.0.1"
|
||||
koa "^2.14.2"
|
||||
koa-static "^5.0.0"
|
||||
open "^7.0.4"
|
||||
portfinder "^1.0.26"
|
||||
replace-in-file "^6.1.0"
|
||||
|
||||
tailwindcss@^3.4.15:
|
||||
version "3.4.15"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.15.tgz#04808bf4bf1424b105047d19e7d4bfab368044a9"
|
||||
@@ -6150,6 +6518,34 @@ tailwindcss@^3.4.15:
|
||||
resolve "^1.22.8"
|
||||
sucrase "^3.35.0"
|
||||
|
||||
tailwindcss@~3.4.13:
|
||||
version "3.4.17"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
||||
integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
|
||||
dependencies:
|
||||
"@alloc/quick-lru" "^5.2.0"
|
||||
arg "^5.0.2"
|
||||
chokidar "^3.6.0"
|
||||
didyoumean "^1.2.2"
|
||||
dlv "^1.1.3"
|
||||
fast-glob "^3.3.2"
|
||||
glob-parent "^6.0.2"
|
||||
is-glob "^4.0.3"
|
||||
jiti "^1.21.6"
|
||||
lilconfig "^3.1.3"
|
||||
micromatch "^4.0.8"
|
||||
normalize-path "^3.0.0"
|
||||
object-hash "^3.0.0"
|
||||
picocolors "^1.1.1"
|
||||
postcss "^8.4.47"
|
||||
postcss-import "^15.1.0"
|
||||
postcss-js "^4.0.1"
|
||||
postcss-load-config "^4.0.2"
|
||||
postcss-nested "^6.2.0"
|
||||
postcss-selector-parser "^6.1.2"
|
||||
resolve "^1.22.8"
|
||||
sucrase "^3.35.0"
|
||||
|
||||
tapable@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
@@ -6282,6 +6678,11 @@ ts-interface-checker@^0.1.9:
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
tsscmp@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
|
||||
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
|
||||
|
||||
turndown@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.0.tgz#67d614fe8371fb511079a93345abfd156c0ffcf4"
|
||||
@@ -6299,6 +6700,14 @@ type-fest@^4.18.2, type-fest@^4.7.1:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
|
||||
integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==
|
||||
|
||||
type-is@^1.6.16:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
dependencies:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
ufo@^1.1.2, ufo@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
|
||||
@@ -6566,6 +6975,11 @@ uuid@^10.0.0:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
|
||||
vary@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
vfile-location@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
|
||||
@@ -6891,7 +7305,7 @@ yargs-parser@^21.1.1:
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^17.5.1:
|
||||
yargs@^17.2.1, yargs@^17.5.1:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
@@ -6904,6 +7318,11 @@ yargs@^17.5.1:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
ylru@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.4.0.tgz#0cf0aa57e9c24f8a2cbde0cc1ca2c9592ac4e0f6"
|
||||
integrity sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==
|
||||
|
||||
zhead@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/zhead/-/zhead-2.2.4.tgz#87cd1e2c3d2f465fa9f43b8db23f9716dfe6bed7"
|
||||
|
||||
Reference in New Issue
Block a user