Merge pull request #18 from Drop-OSS/develop

Merge develop into main
This commit is contained in:
quexeky
2025-01-25 19:27:22 +11:00
committed by GitHub
77 changed files with 3357 additions and 802 deletions

View File

@@ -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
View 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
View File

@@ -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"
}
]
}
}

View File

@@ -2,6 +2,7 @@
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<ModalStack />
</template>
<script setup lang="ts">

View 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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>();

View File

6
composables/icons.ts Normal file
View File

@@ -0,0 +1,6 @@
import { IconsLinuxLogo, IconsWindowsLogo } from "#components";
export const PLATFORM_ICONS = {
[PlatformClient.Linux]: IconsLinuxLogo,
[PlatformClient.Windows]: IconsWindowsLogo,
};

View File

@@ -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];
};

View File

@@ -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

Submodule drop-base added at 01fd41c65a

View File

@@ -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) {

View File

@@ -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`;

View File

@@ -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({
},
},
},
});
});

View File

@@ -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",

View File

@@ -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)
);

View File

@@ -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

View File

@@ -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.";
})

View File

@@ -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=""
/>

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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("");

View File

@@ -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,

View File

@@ -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 });

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Game" ADD COLUMN "mImageCarousel" INTEGER[];

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Game" ALTER COLUMN "mImageCarousel" SET DATA TYPE TEXT[];

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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])
}

View File

@@ -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

View File

@@ -12,4 +12,4 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
}

View File

@@ -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[]

View 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;
});

View File

@@ -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);
}
});

View File

@@ -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;
});

View File

@@ -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,

View 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;
});

View File

@@ -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,
},
});

View File

@@ -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}`,
};
});

View 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;
});

View 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;
});

View 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;
});

View File

@@ -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
},

View 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 {};
});

View 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 {};
});

View 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;
});

View 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;
});

View 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 {};
});

View 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 {};
});

View 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;
});

View 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;
});

View File

@@ -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;

View 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.

View 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;

View File

@@ -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!");

View File

@@ -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),

View 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.");
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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 = {

View 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;

View File

@@ -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);

View File

@@ -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;
});
});

View File

@@ -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)}`
);
}
});

View File

@@ -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
View File

@@ -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"