3 Commits

Author SHA1 Message Date
f8c3883675 chore(deps): bump js-yaml from 4.1.0 to 4.1.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-14 22:03:12 +00:00
dfa30c8a65 Admin home page #128 (#259)
* First iteration on the new PieChart component

* #128 Adds new admin home page

* Fixes code after merging conflicts

* Removes empty file

* Uses real data for admin home page, and improves style

* Reverts debugging code

* Defines missing variable

* Caches user stats data for admin home page

* Typo

* Styles improvements

* Invalidates cache on signup/signin

* Implements top 5 biggest games

* Improves styling

* Improves style

* Using generateManifest to get the proper size

* Reading data from cache

* Removes unnecessary import

* Improves caching mechanism for game sizes

* Removes lint errors

* Replaces piechart tooltip with colors in legend

* Fixes caching

* Fixes caching and slight improvement on pie chart colours

* Fixes a few bugs related to caching

* Fixes bug where app signin didn't refresh cache

* feat: style improvements

* fix: lint

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
2025-11-08 09:14:45 +11:00
289034d0c8 Add manual release date editor (#262)
* add manual release date editor

* watch() releaseDate instead of relying on coreMetadata updates

* make linter happy

---------

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-11-07 09:27:37 +11:00
41 changed files with 1582 additions and 329 deletions

View File

@ -29,6 +29,23 @@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-8">
<MultiItemSelector v-model="currentTags" :items="tags" />
<div class="flex flex-col">
<label
for="releaseDate"
class="text-sm/6 font-medium text-zinc-100"
>
{{ $t("library.admin.game.editReleaseDate") }}
</label>
<div class="mt-2">
<input
id="releaseDate"
v-model="releaseDate"
type="date"
name="releaseDate"
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
/>
</div>
</div>
</div>
<!-- image carousel pick -->
@ -491,11 +508,38 @@ watch(
{ deep: true },
);
const releaseDate = ref(
game.value.mReleased
? new Date(game.value.mReleased).toISOString().substring(0, 10)
: "",
);
watch(releaseDate, async (newDate) => {
const body: PatchGameBody = {};
if (newDate) {
const parsed = new Date(newDate);
if (!isNaN(parsed.getTime())) {
body.mReleased = parsed;
}
}
await $dropFetch(`/api/v1/admin/game/:id`, {
method: "PATCH",
params: {
id: game.value.id,
},
body,
failTitle: "Failed to update release date",
});
});
const { t } = useI18n();
// I don't know why I split these fields off.
const coreMetadataName = ref(game.value.mName);
const coreMetadataDescription = ref(game.value.mShortDescription);
const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
const coreMetadataIconFileUpload = ref<FileList | undefined>();
const coreMetadataLoading = ref(false);
@ -561,7 +605,6 @@ function coreMetadataUpdate_wrapper() {
);
})
.then((newGame) => {
console.log(newGame);
if (!newGame) return;
Object.assign(game.value, newGame);
coreMetadataIconUrl.value = useObject(newGame.mIconObjectId);

View File

@ -51,14 +51,19 @@
@update="() => updateVersionOrder()"
>
<template
#item="{ element: item }: { element: GameVersionModel }"
#item="{ element: item }: { element: GameVersionModelWithSize }"
>
<div
class="w-full inline-flex items-center px-4 py-2 bg-zinc-800 rounded justify-between"
class="w-full inline-flex items-center px-4 py-2 bg-zinc-800 rounded justify-between w-full flex"
>
<div class="text-zinc-100 font-semibold">
<div class="text-zinc-100 font-semibold flex-none">
{{ item.versionName }}
</div>
<div
class="text-right text-zinc-400 text-xs font-normal flex-auto pr-4"
>
{{ item.size && formatBytes(item.size) }}
</div>
<div class="text-zinc-400">
{{ item.delta ? $t("library.admin.version.delta") : "" }}
</div>
@ -117,6 +122,7 @@ import { Bars3Icon, TrashIcon } from "@heroicons/vue/24/solid";
import type { SerializeObject } from "nitropack";
import type { H3Error } from "h3";
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
import { formatBytes } from "~/server/internal/utils/files";
// TODO implement UI for this page
@ -130,7 +136,11 @@ const canImport = computed(
() => hasDeleted.value || props.unimportedVersions.length > 0,
);
type GameAndVersions = GameModel & { versions: GameVersionModel[] };
type GameVersionModelWithSize = GameVersionModel & { size: number };
type GameAndVersions = GameModel & {
versions: GameVersionModelWithSize[];
};
const game = defineModel<SerializeObject<GameAndVersions>>() as Ref<
SerializeObject<GameAndVersions>
>;

View File

@ -0,0 +1,19 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="6" y1="11" x2="10" y2="11" />
<line x1="8" y1="9" x2="8" y2="13" />
<line x1="15" y1="12" x2="15.01" y2="12" />
<line x1="18" y1="10" x2="18.01" y2="10" />
<path
d="M17.32 5H6.68a4 4 0 00-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 003 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 019.828 16h4.344a2 2 0 011.414.586L17 18c.5.5 1 1 2 1a3 3 0 003-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0017.32 5z"
/>
</svg>
</template>

View File

@ -0,0 +1,45 @@
<template>
<h2 v-if="title" class="text-lg mb-4 w-full">{{ title }}</h2>
<div class="flex flex-col xl:flex-row gap-4">
<div class="relative flex grow max-w-[12rem]">
<svg class="aspect-square grow relative inline" viewBox="0 0 100 100">
<PieChartPieSlice
v-for="slice in slices"
:key="`${slice.percentage}-${slice.totalPercentage}`"
:slice="slice"
/>
</svg>
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
</div>
<ul class="flex flex-col gap-y-1 justify-center text-left">
<li
v-for="slice in slices"
:key="slice.value"
class="text-sm inline-flex items-center gap-x-1"
>
<span
class="size-3 inline-block rounded-sm"
:class="CHART_COLOURS[slice.color].bg"
/>
{{
$t("common.labelValueColon", {
label: slice.label,
value: slice.value,
})
}}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { generateSlices } from "~/components/PieChart/utils";
import type { SliceData } from "~/components/PieChart/types";
const { data, title = undefined } = defineProps<{
data: SliceData[];
title?: string | undefined;
}>();
const slices = generateSlices(data);
</script>

View File

@ -0,0 +1,35 @@
<template>
<path
v-if="slice.percentage !== 0 && slice.percentage !== 100"
:class="[CHART_COLOURS[slice.color].fill]"
:d="`
M ${slice.start}
A ${slice.radius},${slice.radius} 0 ${getFlags(slice.percentage)} ${polarToCartesian(slice.center, slice.radius, percent2Degrees(slice.totalPercentage))}
L ${slice.center}
z
`"
stroke-width="2"
/>
<circle
v-if="slice.percentage === 100"
:r="slice.radius"
:cx="slice.center.x"
:cy="slice.center.y"
:class="[CHART_COLOURS[slice.color].fill]"
stroke-width="2"
/>
</template>
<script setup lang="ts">
import type { Slice } from "~/components/PieChart/types";
import {
getFlags,
percent2Degrees,
polarToCartesian,
} from "~/components/PieChart/utils";
import { CHART_COLOURS } from "~/utils/colors";
const { slice } = defineProps<{
slice: Slice;
}>();
</script>

19
components/PieChart/types.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import type Tuple from "~/utils/tuple";
import type { ChartColour } from "~/utils/colors";
export type Slice = {
start: Tuple;
center: Tuple;
percentage: number;
totalPercentage: number;
radius: number;
color: ChartColour;
label: string;
value: number;
};
export type SliceData = {
value: number;
color?: ChartColour;
label: string;
};

View File

@ -0,0 +1,50 @@
import Tuple from "~/utils/tuple";
import type { Slice, SliceData } from "~/components/PieChart/types";
import { sum, lastItem } from "~/utils/array";
export const START = new Tuple(50, 10);
export const CENTER = new Tuple(50, 50);
export const RADIUS = 40;
export const polarToCartesian = (
center: Tuple,
radius: number,
angleInDegrees: number,
) => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180;
const x = center.x + radius * Math.cos(angleInRadians);
const y = center.y + radius * Math.sin(angleInRadians);
return new Tuple(x, y);
};
export const percent2Degrees = (percentage: number) => (360 * percentage) / 100;
export function generateSlices(data: SliceData[]): Slice[] {
return data.reduce((accumulator, currentValue, index, array) => {
const percentage =
(currentValue.value * 100) / sum(array.map((slice) => slice.value));
return [
...accumulator,
{
start: accumulator.length
? polarToCartesian(
CENTER,
RADIUS,
percent2Degrees(lastItem(accumulator).totalPercentage),
)
: START,
radius: RADIUS,
percentage: percentage,
totalPercentage:
sum(accumulator.map((element) => element.percentage)) + percentage,
center: CENTER,
color: PIE_COLOURS[index % PIE_COLOURS.length],
label: currentValue.label,
value: currentValue.value,
},
];
}, [] as Slice[]);
}
export const getFlags = (percentage: number) =>
percentage > 50 ? new Tuple(1, 1) : new Tuple(0, 1);

View File

@ -0,0 +1,31 @@
<template>
<div
:class="[
'relative h-5 rounded-xl overflow-hidden',
CHART_COLOURS[backgroundColor].bg,
]"
>
<div
:style="{ width: `${percentage}%` }"
:class="['transition-all h-full', CHART_COLOURS[color].bg]"
/>
<span
class="absolute inset-0 flex items-center justify-center text-blue-200 text-sm font-bold font-display"
>
{{ $t("tasks.admin.progress", [Math.round(percentage * 10) / 10]) }}
</span>
</div>
</template>
<script setup lang="ts">
import { type ChartColour, CHART_COLOURS } from "~/utils/colors";
const {
percentage,
color = "blue",
backgroundColor = "zinc",
} = defineProps<{
percentage: number;
color?: ChartColour;
backgroundColor?: ChartColour;
}>();
</script>

View File

@ -0,0 +1,43 @@
<template>
<table v-if="items.length > 0" class="w-full mt-4 space-y-6">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
<tr v-for="item in items" :key="`${item.rank}-${item.name}`">
<td
class="my-2 size-7 rounded-sm bg-zinc-950 ring ring-zinc-800 inline-flex items-center justify-center font-bold font-display text-blue-500"
>
{{ item.rank }}
</td>
<td class="w-full font-bold px-2">{{ item.name }}</td>
<td
class="text-right text-sm font-semibold text-zinc-500 whitespace-nowrap"
>
{{ item.value }}
</td>
</tr>
</tbody>
</table>
<p
v-else
class="w-full p-2 text-center uppercase text-sm font-display font-bold text-zinc-700"
>
{{ $t("common.noData") }}
</p>
</template>
<script lang="ts" setup>
export type RankItem = {
rank: number;
name: string;
value: string;
};
const { items } = defineProps<{
items: RankItem[];
}>();
</script>

193
components/SourceTable.vue Normal file
View File

@ -0,0 +1,193 @@
<template>
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-zinc-700">
<thead>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
>
{{ $t("common.name") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("type") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("library.admin.sources.working") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("options") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("library.admin.sources.totalSpace") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("library.admin.sources.freeSpace") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("library.admin.sources.utilizationPercentage") }}
</th>
<th
v-if="editSource || deleteSource"
scope="col"
class="relative py-3.5 pl-3 pr-4 sm:pr-3"
>
<span class="sr-only">{{ $t("actions") }}</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(source, sourceIdx) in sources"
:key="source.id"
class="even:bg-zinc-800"
>
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ source.name }}
</td>
<td
class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400 inline-flex gap-x-1 items-center"
>
<component
:is="optionsMetadata[source.backend].icon"
class="size-5 text-zinc-400"
/>
{{ optionsMetadata[source.backend].title }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
<CheckIcon v-if="source.working" class="size-5 text-green-500" />
<XMarkIcon v-else class="size-5 text-red-500" />
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ source.options }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ source.fsStats && formatBytes(source.fsStats.totalSpace) }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ source.fsStats && formatBytes(source.fsStats.freeSpace) }}
</td>
<td
class="align-middle flex flex-cols-5 whitespace-nowrap px-3 py-4 text-sm text-zinc-400"
>
<div class="flex-auto content-right">
<ProgressBar
v-if="source.fsStats"
:percentage="
getPercentage(
source.fsStats.freeSpace,
source.fsStats.totalSpace,
)
"
:color="
getBarColor(
getPercentage(
source.fsStats.freeSpace,
source.fsStats.totalSpace,
),
)
"
background-color="slate"
/>
</div>
</td>
<td
v-if="editSource || deleteSource"
class="relative whitespace-nowrap py-4 pl-3 pr-3 text-right text-sm font-medium space-x-2"
>
<button
v-if="editSource"
class="text-blue-500 hover:text-blue-400"
@click="() => editSource(sourceIdx)"
>
{{ $t("common.edit") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
<button
v-if="deleteSource"
class="text-red-500 hover:text-red-400"
@click="() => deleteSource(sourceIdx)"
>
{{ $t("delete") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
import type { LibraryBackend } from "~/prisma/client/enums";
import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
import { DropLogo } from "#components";
import { formatBytes } from "~/server/internal/utils/files";
import { getBarColor } from "~/utils/colors";
const {
sources,
deleteSource = undefined,
editSource = undefined,
} = defineProps<{
sources: WorkingLibrarySource[];
summaryMode?: boolean;
deleteSource?: (id: number) => void;
editSource?: (id: number) => void;
}>();
const { t } = useI18n();
const optionsMetadata: {
[key in LibraryBackend]: {
title: string;
description: string;
docsLink: string;
icon: Component;
};
} = {
Filesystem: {
title: t("library.admin.sources.fsTitle"),
description: t("library.admin.sources.fsDesc"),
docsLink: "https://docs.droposs.org/docs/library#drop-style",
icon: DropLogo,
},
FlatFilesystem: {
title: t("library.admin.sources.fsFlatTitle"),
description: t("library.admin.sources.fsFlatDesc"),
docsLink: "https://docs.droposs.org/docs/library#flat-style-or-compat",
icon: BackwardIcon,
},
};
const getPercentage = (value: number, total: number) =>
((total - value) * 100) / total;
</script>

View File

@ -0,0 +1,52 @@
<template>
<div
:class="[
'border border-zinc-800 rounded-xl h-full px-6 py-4 relative bg-zinc-950/30',
{ 'min-h-50 pb-15': link, 'lg:pb-4': !link },
]"
>
<h1
v-if="props.title"
:class="[
'font-semibold text-lg w-full',
{ 'mb-3': !props.subtitle && link },
]"
>
{{ props.title }}
<div v-if="rightTitle" class="float-right">{{ props.rightTitle }}</div>
</h1>
<h2
v-if="props.subtitle"
:class="['text-zinc-400 text-sm w-full', { 'mb-3': link }]"
>
{{ props.subtitle }}
<div v-if="rightTitle" class="float-right">{{ props.rightTitle }}</div>
</h2>
<slot />
<div v-if="props.link" class="absolute bottom-5 right-5">
<NuxtLink
:to="props.link.url"
class="transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
{{ props.link.label }}
<ArrowRightIcon class="h-4 w-4" aria-hidden="true" />
</NuxtLink>
</div>
</div>
</template>
<script lang="ts" setup>
import { ArrowRightIcon } from "@heroicons/vue/20/solid";
const props = defineProps<{
title?: string;
subtitle?: string;
rightTitle?: string;
link?: {
url: string;
label: string;
};
}>();
</script>

View File

@ -117,7 +117,9 @@
"servers": "Servers",
"srLoading": "Loading…",
"tags": "Tags",
"today": "Today"
"today": "Today",
"labelValueColon": "{label}: {value}",
"noData": "No data"
},
"delete": "Delete",
"drop": {
@ -268,6 +270,8 @@
"store": "Store",
"tokens": "API tokens"
},
"home": "Home",
"library": "Library",
"tasks": "Tasks",
"users": "Users"
},
@ -276,7 +280,24 @@
},
"helpUsTranslate": "Help us translate Drop {arrow}",
"highest": "highest",
"home": "Home",
"home": {
"admin": {
"title": "Home",
"subheader": "Instance summary",
"games": "Games",
"librarySources": "Library sources",
"version": "Version",
"activeInactiveUsers": "Active/inactive users",
"activeUsers": "Active users",
"inactiveUsers": "Inactive users",
"goToUsers": "Go to users",
"users": "Users",
"biggestGamesToDownload": "Biggest games to download",
"latestVersionOnly": "Latest version only",
"biggestGamesOnServer": "Biggest games on server",
"allVersionsCombined": "All versions combined"
}
},
"library": {
"addGames": "All Games",
"addToLib": "Add to Library",
@ -292,6 +313,7 @@
"deleteImage": "Delete image",
"editGameDescription": "Game Description",
"editGameName": "Game Name",
"editReleaseDate": "Release Date",
"imageCarousel": "Image Carousel",
"imageCarouselDescription": "Customise what images and what order are shown on the store page.",
"imageCarouselEmpty": "No images added to the carousel yet.",
@ -423,7 +445,11 @@
"namePlaceholder": "My New Source",
"sources": "Library Sources",
"typeDesc": "The type of your source. Changes the required options.",
"working": "Working?"
"working": "Working?",
"freeSpace": "Free space",
"totalSpace": "Total space",
"utilizationPercentage": "Utilization percentage",
"percentage": "{number}%"
},
"subheader": "As you add folders to your library sources, Drop will detect it and prompt you to import it. Each game needs to be imported before you can import a version.",
"title": "Libraries",
@ -553,6 +579,7 @@
"openFeatured": "Star games in Admin Library {arrow}",
"platform": "Platform | Platform | Platforms",
"publishers": "Publishers | Publisher | Publishers",
"size": "Size",
"rating": "Rating",
"readLess": "Click to read less",
"readMore": "Click to read more",

View File

@ -174,9 +174,14 @@ import { XMarkIcon } from "@heroicons/vue/24/solid";
const i18nHead = useLocaleHead();
const navigation: Array<NavigationItem & { icon: Component }> = [
{ label: $t("home"), route: "/admin", prefix: "/admin", icon: HomeIcon },
{
label: $t("userHeader.links.library"),
label: $t("header.admin.home"),
route: "/admin",
prefix: "/admin",
icon: HomeIcon,
},
{
label: $t("header.admin.library"),
route: "/admin/library",
prefix: "/admin/library",
icon: ServerStackIcon,

View File

@ -1,6 +1,147 @@
<template><div /></template>
<template>
<div class="space-y-4">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-zinc-100">
{{ t("home.admin.title") }}
</h1>
<p class="mt-2 text-base text-zinc-400">
{{ t("home.admin.subheader") }}
</p>
</div>
</div>
<main
class="mx-auto max-w-md lg:max-w-none md:max-w-none w-full py-2 text-zinc-100"
>
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1">
<TileWithLink>
<div class="h-full flex">
<div class="flex-1 my-auto">
<DropLogo />
</div>
<div
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
>
<div class="text-2xl flex-1 font-bold">{{ version }}</div>
<div class="text-xs flex-1 text-left lg:text-center">
{{ t("home.admin.version") }}
</div>
</div>
</div>
</TileWithLink>
</div>
<div class="col-span-6 lg:col-span-1 md:col-span-3">
<TileWithLink>
<div class="h-full flex">
<div class="flex-1 my-auto">
<GamepadIcon />
</div>
<div
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
>
<div class="text-3xl flex-1 font-bold">{{ gameCount }}</div>
<div class="text-xs flex-1 text-left lg:text-center">
{{ t("home.admin.games") }}
</div>
</div>
</div>
</TileWithLink>
</div>
<div
class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1 lg:col-start-1 lg:row-start-2"
>
<TileWithLink>
<div class="h-full flex">
<div class="flex-1 my-auto">
<ServerStackIcon />
</div>
<div
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
>
<div class="text-3xl flex-1 font-bold">
{{ sources.length }}
</div>
<div class="text-xs flex-1 text-left lg:text-center">
{{ t("home.admin.librarySources") }}
</div>
</div>
</div>
</TileWithLink>
</div>
<div
class="col-span-6 lg:col-span-1 md:col-span-3 row-span-1 lg:col-start-2 lg:row-start-2"
>
<TileWithLink>
<div class="h-full flex">
<div class="flex-1 my-auto">
<UserGroupIcon />
</div>
<div
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
>
<div class="text-3xl flex-1 font-bold">
{{ userStats.userCount }}
</div>
<div class="text-xs flex-1 text-left lg:text-center">
{{ t("home.admin.users") }}
</div>
</div>
</div>
</TileWithLink>
</div>
<div class="col-span-6 row-span-1 lg:col-span-2 lg:row-span-2">
<TileWithLink
:link="{
url: '/admin/users',
label: t('home.admin.goToUsers'),
}"
:title="t('home.admin.activeInactiveUsers')"
>
<PieChart :data="pieChartData" />
</TileWithLink>
</div>
<div class="col-span-6">
<TileWithLink
title="Library"
:link="{ url: '/admin/library', label: 'Go to library' }"
>
<SourceTable :sources="sources" />
</TileWithLink>
</div>
<div class="col-span-6 lg:col-span-2">
<TileWithLink
:title="t('home.admin.biggestGamesToDownload')"
:subtitle="t('home.admin.latestVersionOnly')"
>
<RankingList :items="biggestGamesLatest.map(gameToRankItem)" />
</TileWithLink>
</div>
<div class="col-span-6 lg:col-span-2">
<TileWithLink
:title="t('home.admin.biggestGamesOnServer')"
:subtitle="t('home.admin.allVersionsCombined')"
>
<RankingList :items="biggestGamesCombined.map(gameToRankItem)" />
</TileWithLink>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { formatBytes } from "~/server/internal/utils/files";
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
import DropLogo from "~/components/DropLogo.vue";
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
import type { GameSize } from "~/server/internal/gamesize";
import type { RankItem } from "~/components/RankingList.vue";
definePageMeta({
layout: "admin",
});
@ -8,4 +149,29 @@ definePageMeta({
useHead({
title: "Home",
});
const { t } = useI18n();
const {
version,
gameCount,
sources,
userStats,
biggestGamesLatest,
biggestGamesCombined,
} = await $dropFetch("/api/v1/admin/home");
const gameToRankItem = (game: GameSize, rank: number): RankItem => ({
rank: rank + 1,
name: game.gameName,
value: formatBytes(game.size),
});
const pieChartData = [
{
label: t("home.admin.inactiveUsers"),
value: userStats.userCount - userStats.activeSessions,
},
{ label: t("home.admin.activeUsers"), value: userStats.activeSessions },
];
</script>

View File

@ -18,99 +18,12 @@
</button>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-zinc-700">
<thead>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
>
{{ $t("common.name") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("type") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("library.admin.sources.working") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
{{ $t("options") }}
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-3">
<span class="sr-only">{{ $t("common.edit") }}</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(source, sourceIdx) in sources"
:key="source.id"
class="even:bg-zinc-800"
>
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ source.name }}
</td>
<td
class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400 inline-flex gap-x-1 items-center"
>
<component
:is="optionsMetadata[source.backend].icon"
class="size-5 text-zinc-400"
/>
{{ optionsMetadata[source.backend].title }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
<CheckIcon
v-if="source.working"
class="size-5 text-green-500"
/>
<XMarkIcon v-else class="size-5 text-red-500" />
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ source.options }}
</td>
<td
class="relative whitespace-nowrap py-4 pl-3 pr-3 text-right text-sm font-medium space-x-2"
>
<button
class="text-blue-500 hover:text-blue-400"
@click="() => edit(sourceIdx)"
>
{{ $t("common.edit") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
<button
class="text-red-500 hover:text-red-400"
@click="() => deleteSource(sourceIdx)"
>
{{ $t("delete") }}
<span class="sr-only">
{{ $t("chars.srComma", [source.name]) }}
</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-4 flow-root">
<SourceTable
:sources="sources"
:edit-source="edit"
:delete-source="deleteSource"
/>
</div>
<ModalTemplate v-model="actionSourceOpen">
@ -313,7 +226,7 @@ import {
XCircleIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/vue/20/solid";
import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
import { BackwardIcon } from "@heroicons/vue/24/outline";
import { FetchError } from "ofetch";
import type { Component } from "vue";
import type { LibraryBackend } from "~/prisma/client/enums";

View File

@ -53,18 +53,7 @@
:log="parseTaskLog(task.log.at(-(idx + 1)))"
/>
</div>
<div class="relative h-5 rounded-xl bg-zinc-950 overflow-hidden">
<div
:style="{ width: `${task.progress}%` }"
class="transition-all bg-blue-600 h-full"
/>
<span
class="absolute inset-0 flex items-center justify-center text-blue-200 text-sm font-bold font-display"
>{{
$t("tasks.admin.progress", [Math.round(task.progress * 10) / 10])
}}</span
>
</div>
<ProgressBar :percentage="task.progress" />
</div>
<div v-else role="status" class="w-full flex items-center justify-center">
<svg

View File

@ -86,6 +86,27 @@
>
</td>
</tr>
<tr>
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ $t("store.size") }}
</td>
<td
v-if="size"
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400"
>
{{ formatBytes(size) }}
</td>
<td
v-else
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400 italic"
>
<span class="font-semibold text-blue-600">{{
$t("store.commingSoon")
}}</span>
</td>
</tr>
<tr>
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
@ -246,13 +267,14 @@ import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
import { StarIcon } from "@heroicons/vue/24/solid";
import { micromark } from "micromark";
import type { PlatformClient } from "~/composables/types";
import { formatBytes } from "~/server/internal/utils/files";
const route = useRoute();
const gameId = route.params.id.toString();
const user = useUser();
const { game, rating } = await $dropFetch(`/api/v1/games/${gameId}`);
const { game, rating, size } = await $dropFetch(`/api/v1/games/${gameId}`);
// Preview description (first 30 lines)
const showPreview = ref(true);

364
pnpm-lock.yaml generated
View File

@ -19,10 +19,10 @@ importers:
version: 3.2.0
'@headlessui/vue':
specifier: ^1.7.23
version: 1.7.23(vue@3.5.22(typescript@5.8.3))
version: 1.7.23(vue@3.5.24(typescript@5.8.3))
'@heroicons/vue':
specifier: ^2.1.5
version: 2.2.0(vue@3.5.22(typescript@5.8.3))
version: 2.2.0(vue@3.5.24(typescript@5.8.3))
'@lobomfz/prismark':
specifier: 0.0.3
version: 0.0.3
@ -34,7 +34,7 @@ importers:
version: 1.10.0(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.7.0)(magicast@0.3.5)
'@nuxtjs/i18n':
specifier: ^9.5.5
version: 9.5.6(@vue/compiler-dom@3.5.22)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(rollup@4.46.2)(vue@3.5.22(typescript@5.8.3))
version: 9.5.6(@vue/compiler-dom@3.5.24)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(rollup@4.46.2)(vue@3.5.24(typescript@5.8.3))
'@prisma/client':
specifier: ^6.11.1
version: 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
@ -43,7 +43,7 @@ importers:
version: 4.1.11(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
'@vueuse/nuxt':
specifier: 13.6.0
version: 13.6.0(magicast@0.3.5)(nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.22)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
version: 13.6.0(magicast@0.3.5)(nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.24)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
argon2:
specifier: ^0.43.0
version: 0.43.1
@ -82,7 +82,7 @@ importers:
version: 8.0.2
nuxt:
specifier: ^3.17.4
version: 3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.22)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0)
version: 3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.24)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0)
nuxt-security:
specifier: 2.2.0
version: 2.2.0(magicast@0.3.5)(rollup@4.46.2)
@ -115,26 +115,26 @@ importers:
version: 3.1.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
vue:
specifier: latest
version: 3.5.22(typescript@5.8.3)
version: 3.5.24(typescript@5.8.3)
vue-router:
specifier: latest
version: 4.5.1(vue@3.5.22(typescript@5.8.3))
version: 4.6.3(vue@3.5.24(typescript@5.8.3))
vue3-carousel:
specifier: ^0.16.0
version: 0.16.0(vue@3.5.22(typescript@5.8.3))
version: 0.16.0(vue@3.5.24(typescript@5.8.3))
vue3-carousel-nuxt:
specifier: ^1.1.5
version: 1.1.6(magicast@0.3.5)(vue@3.5.22(typescript@5.8.3))
version: 1.1.6(magicast@0.3.5)(vue@3.5.24(typescript@5.8.3))
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.22(typescript@5.8.3))
version: 4.1.0(vue@3.5.24(typescript@5.8.3))
devDependencies:
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.0.1
version: 4.0.1(eslint@9.31.0(jiti@2.5.1))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.5.1)))(yaml-eslint-parser@1.3.0)
'@nuxt/eslint':
specifier: ^1.3.0
version: 1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
version: 1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
'@tailwindcss/forms':
specifier: ^0.5.9
version: 0.5.10(tailwindcss@4.1.11)
@ -283,6 +283,10 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
@ -296,8 +300,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.28.4':
resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
'@babel/parser@7.28.5':
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
engines: {node: '>=6.0.0'}
hasBin: true
@ -339,8 +343,8 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.4':
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
'@babel/types@7.28.5':
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@capsizecss/metrics@3.5.0':
@ -2084,26 +2088,26 @@ packages:
'@vue/compiler-core@3.5.18':
resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==}
'@vue/compiler-core@3.5.22':
resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
'@vue/compiler-core@3.5.24':
resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==}
'@vue/compiler-dom@3.5.18':
resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==}
'@vue/compiler-dom@3.5.22':
resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==}
'@vue/compiler-dom@3.5.24':
resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==}
'@vue/compiler-sfc@3.5.18':
resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==}
'@vue/compiler-sfc@3.5.22':
resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==}
'@vue/compiler-sfc@3.5.24':
resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==}
'@vue/compiler-ssr@3.5.18':
resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==}
'@vue/compiler-ssr@3.5.22':
resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==}
'@vue/compiler-ssr@3.5.24':
resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
@ -2130,25 +2134,25 @@ packages:
typescript:
optional: true
'@vue/reactivity@3.5.22':
resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==}
'@vue/reactivity@3.5.24':
resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==}
'@vue/runtime-core@3.5.22':
resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==}
'@vue/runtime-core@3.5.24':
resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==}
'@vue/runtime-dom@3.5.22':
resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==}
'@vue/runtime-dom@3.5.24':
resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==}
'@vue/server-renderer@3.5.22':
resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==}
'@vue/server-renderer@3.5.24':
resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==}
peerDependencies:
vue: 3.5.22
vue: 3.5.24
'@vue/shared@3.5.18':
resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==}
'@vue/shared@3.5.22':
resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
'@vue/shared@3.5.24':
resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==}
'@vueuse/core@13.6.0':
resolution: {integrity: sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==}
@ -2718,8 +2722,8 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.0:
resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
@ -3961,8 +3965,8 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
@ -5709,10 +5713,10 @@ packages:
peerDependencies:
vue: ^3.0.0
vue-router@4.5.1:
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
vue-router@4.6.3:
resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==}
peerDependencies:
vue: ^3.2.0
vue: ^3.5.0
vue-tsc@3.0.4:
resolution: {integrity: sha512-kZmSEjGtROApVBuaIcoprrXZsFNGon5ggkTJokmhQ/H1hMzCFRPQ0Ed8IHYFsmYJYvHBcdmEQVGVcRuxzPzNbw==}
@ -5733,8 +5737,8 @@ packages:
peerDependencies:
vue: ^3.5.0
vue@3.5.22:
resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==}
vue@3.5.24:
resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@ -5946,7 +5950,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@ -5974,7 +5978,7 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.4
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
@ -5996,7 +6000,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@babel/helper-plugin-utils@7.27.1': {}
@ -6012,7 +6016,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.4
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
@ -6020,6 +6024,8 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.28.2':
@ -6031,9 +6037,9 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
'@babel/parser@7.28.4':
'@babel/parser@7.28.5':
dependencies:
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)':
dependencies:
@ -6086,10 +6092,10 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/types@7.28.4':
'@babel/types@7.28.5':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@capsizecss/metrics@3.5.0': {}
@ -6434,14 +6440,14 @@ snapshots:
'@fastify/busboy@3.1.1': {}
'@headlessui/vue@1.7.23(vue@3.5.22(typescript@5.8.3))':
'@headlessui/vue@1.7.23(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@tanstack/vue-virtual': 3.13.12(vue@3.5.22(typescript@5.8.3))
vue: 3.5.22(typescript@5.8.3)
'@tanstack/vue-virtual': 3.13.12(vue@3.5.24(typescript@5.8.3))
vue: 3.5.24(typescript@5.8.3)
'@heroicons/vue@2.2.0(vue@3.5.22(typescript@5.8.3))':
'@heroicons/vue@2.2.0(vue@3.5.24(typescript@5.8.3))':
dependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@humanfs/core@0.19.1': {}
@ -6456,7 +6462,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@intlify/bundle-utils@10.0.1(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))':
'@intlify/bundle-utils@10.0.1(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))':
dependencies:
'@intlify/message-compiler': 11.1.11
'@intlify/shared': 11.1.11
@ -6468,7 +6474,7 @@ snapshots:
source-map-js: 1.2.1
yaml-eslint-parser: 1.3.0
optionalDependencies:
vue-i18n: 10.0.8(vue@3.5.22(typescript@5.8.3))
vue-i18n: 10.0.8(vue@3.5.24(typescript@5.8.3))
'@intlify/core-base@10.0.8':
dependencies:
@ -6529,12 +6535,12 @@ snapshots:
'@intlify/shared@11.1.11': {}
'@intlify/unplugin-vue-i18n@6.0.8(@vue/compiler-dom@3.5.22)(eslint@9.31.0(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))':
'@intlify/unplugin-vue-i18n@6.0.8(@vue/compiler-dom@3.5.24)(eslint@9.31.0(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.5.1))
'@intlify/bundle-utils': 10.0.1(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))
'@intlify/bundle-utils': 10.0.1(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))
'@intlify/shared': 11.1.11
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.11)(@vue/compiler-dom@3.5.22)(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.11)(@vue/compiler-dom@3.5.24)(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
'@typescript-eslint/scope-manager': 8.38.0
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
@ -6546,9 +6552,9 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
unplugin: 1.16.1
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
optionalDependencies:
vue-i18n: 10.0.8(vue@3.5.22(typescript@5.8.3))
vue-i18n: 10.0.8(vue@3.5.24(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/compiler-dom'
- eslint
@ -6558,14 +6564,14 @@ snapshots:
'@intlify/utils@0.13.0': {}
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.11)(@vue/compiler-dom@3.5.22)(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.11)(@vue/compiler-dom@3.5.24)(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@babel/parser': 7.28.0
optionalDependencies:
'@intlify/shared': 11.1.11
'@vue/compiler-dom': 3.5.22
vue: 3.5.22(typescript@5.8.3)
vue-i18n: 10.0.8(vue@3.5.22(typescript@5.8.3))
'@vue/compiler-dom': 3.5.24
vue: 3.5.24(typescript@5.8.3)
vue-i18n: 10.0.8(vue@3.5.24(typescript@5.8.3))
'@ioredis/commands@1.3.0': {}
@ -6814,12 +6820,12 @@ snapshots:
prompts: 2.4.2
semver: 7.7.2
'@nuxt/devtools@2.6.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))':
'@nuxt/devtools@2.6.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
'@nuxt/devtools-wizard': 2.6.2
'@nuxt/kit': 3.18.0(magicast@0.3.5)
'@vue/devtools-core': 7.7.7(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
'@vue/devtools-core': 7.7.7(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
'@vue/devtools-kit': 7.7.7
birpc: 2.5.0
consola: 3.4.2
@ -6846,7 +6852,7 @@ snapshots:
tinyglobby: 0.2.14
vite: 7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vite-plugin-inspect: 11.3.2(@nuxt/kit@3.18.0(magicast@0.3.5))(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
vite-plugin-vue-tracer: 1.0.0(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
vite-plugin-vue-tracer: 1.0.0(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
which: 5.0.0
ws: 8.18.3
transitivePeerDependencies:
@ -6855,7 +6861,7 @@ snapshots:
- utf-8-validate
- vue
'@nuxt/eslint-config@1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)':
'@nuxt/eslint-config@1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.11.0
@ -6874,7 +6880,7 @@ snapshots:
eslint-plugin-regexp: 2.9.0(eslint@9.31.0(jiti@2.5.1))
eslint-plugin-unicorn: 60.0.0(eslint@9.31.0(jiti@2.5.1))
eslint-plugin-vue: 10.4.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.31.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.5.1)))
eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1))
eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1))
globals: 16.3.0
local-pkg: 1.1.1
pathe: 2.0.3
@ -6895,11 +6901,11 @@ snapshots:
- supports-color
- typescript
'@nuxt/eslint@1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))':
'@nuxt/eslint@1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))':
dependencies:
'@eslint/config-inspector': 1.1.0(eslint@9.31.0(jiti@2.5.1))
'@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
'@nuxt/eslint-config': 1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)
'@nuxt/eslint-config': 1.7.1(@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)
'@nuxt/eslint-plugin': 1.7.1(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)
'@nuxt/kit': 4.0.2(magicast@0.3.5)
chokidar: 4.0.3
@ -7109,12 +7115,12 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxt/vite-builder@3.17.7(@types/node@22.16.5)(eslint@9.31.0(jiti@2.5.1))(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vue-tsc@3.0.4(typescript@5.8.3))(vue@3.5.22(typescript@5.8.3))(yaml@2.8.0)':
'@nuxt/vite-builder@3.17.7(@types/node@22.16.5)(eslint@9.31.0(jiti@2.5.1))(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vue-tsc@3.0.4(typescript@5.8.3))(vue@3.5.24(typescript@5.8.3))(yaml@2.8.0)':
dependencies:
'@nuxt/kit': 3.17.7(magicast@0.3.5)
'@rollup/plugin-replace': 6.0.2(rollup@4.46.2)
'@vitejs/plugin-vue': 5.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
'@vitejs/plugin-vue-jsx': 4.2.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
'@vitejs/plugin-vue': 5.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
'@vitejs/plugin-vue-jsx': 4.2.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
autoprefixer: 10.4.21(postcss@8.5.6)
consola: 3.4.2
cssnano: 7.1.0(postcss@8.5.6)
@ -7142,7 +7148,7 @@ snapshots:
vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vite-node: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vite-plugin-checker: 0.10.1(eslint@9.31.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue-bundle-renderer: 2.1.2
transitivePeerDependencies:
- '@biomejs/biome'
@ -7169,11 +7175,11 @@ snapshots:
- vue-tsc
- yaml
'@nuxtjs/i18n@9.5.6(@vue/compiler-dom@3.5.22)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(rollup@4.46.2)(vue@3.5.22(typescript@5.8.3))':
'@nuxtjs/i18n@9.5.6(@vue/compiler-dom@3.5.24)(eslint@9.31.0(jiti@2.5.1))(magicast@0.3.5)(rollup@4.46.2)(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@intlify/h3': 0.6.1
'@intlify/shared': 10.0.8
'@intlify/unplugin-vue-i18n': 6.0.8(@vue/compiler-dom@3.5.22)(eslint@9.31.0(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))
'@intlify/unplugin-vue-i18n': 6.0.8(@vue/compiler-dom@3.5.24)(eslint@9.31.0(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))
'@intlify/utils': 0.13.0
'@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.46.2)
'@nuxt/kit': 3.18.0(magicast@0.3.5)
@ -7193,9 +7199,9 @@ snapshots:
typescript: 5.8.3
ufo: 1.6.1
unplugin: 2.3.5
unplugin-vue-router: 0.12.0(vue-router@4.5.1(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))
vue-i18n: 10.0.8(vue@3.5.22(typescript@5.8.3))
vue-router: 4.5.1(vue@3.5.22(typescript@5.8.3))
unplugin-vue-router: 0.12.0(vue-router@4.6.3(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))
vue-i18n: 10.0.8(vue@3.5.24(typescript@5.8.3))
vue-router: 4.6.3(vue@3.5.24(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/compiler-dom'
- eslint
@ -7673,10 +7679,10 @@ snapshots:
'@tanstack/virtual-core@3.13.12': {}
'@tanstack/vue-virtual@3.13.12(vue@3.5.22(typescript@5.8.3))':
'@tanstack/vue-virtual@3.13.12(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@tanstack/virtual-core': 3.13.12
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@tokenizer/token@0.3.0': {}
@ -7824,11 +7830,11 @@ snapshots:
'@typescript-eslint/types': 8.38.0
eslint-visitor-keys: 4.2.1
'@unhead/vue@2.0.13(vue@3.5.22(typescript@5.8.3))':
'@unhead/vue@2.0.13(vue@3.5.24(typescript@5.8.3))':
dependencies:
hookable: 5.5.3
unhead: 2.0.13
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
optional: true
@ -7908,21 +7914,21 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))':
'@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@babel/core': 7.28.0
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@rolldown/pluginutils': 1.0.0-beta.29
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.28.0)
vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))':
'@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))':
dependencies:
vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@volar/language-core@2.4.20':
dependencies:
@ -7936,7 +7942,7 @@ snapshots:
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue-macros/common@1.16.1(vue@3.5.22(typescript@5.8.3))':
'@vue-macros/common@1.16.1(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@vue/compiler-sfc': 3.5.18
ast-kit: 1.4.3
@ -7945,17 +7951,17 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
optionalDependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@vue-macros/common@3.0.0-beta.15(vue@3.5.22(typescript@5.8.3))':
'@vue-macros/common@3.0.0-beta.15(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@vue/compiler-sfc': 3.5.22
'@vue/compiler-sfc': 3.5.24
ast-kit: 2.1.1
local-pkg: 1.1.1
magic-string-ast: 1.0.0
unplugin-utils: 0.2.4
optionalDependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@vue/babel-helper-vue-transform-on@1.4.0': {}
@ -7981,8 +7987,8 @@ snapshots:
'@babel/core': 7.28.0
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/parser': 7.28.4
'@vue/compiler-sfc': 3.5.22
'@babel/parser': 7.28.5
'@vue/compiler-sfc': 3.5.24
transitivePeerDependencies:
- supports-color
@ -7994,10 +8000,10 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.22':
'@vue/compiler-core@3.5.24':
dependencies:
'@babel/parser': 7.28.4
'@vue/shared': 3.5.22
'@babel/parser': 7.28.5
'@vue/shared': 3.5.24
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
@ -8007,10 +8013,10 @@ snapshots:
'@vue/compiler-core': 3.5.18
'@vue/shared': 3.5.18
'@vue/compiler-dom@3.5.22':
'@vue/compiler-dom@3.5.24':
dependencies:
'@vue/compiler-core': 3.5.22
'@vue/shared': 3.5.22
'@vue/compiler-core': 3.5.24
'@vue/shared': 3.5.24
'@vue/compiler-sfc@3.5.18':
dependencies:
@ -8024,15 +8030,15 @@ snapshots:
postcss: 8.5.6
source-map-js: 1.2.1
'@vue/compiler-sfc@3.5.22':
'@vue/compiler-sfc@3.5.24':
dependencies:
'@babel/parser': 7.28.4
'@vue/compiler-core': 3.5.22
'@vue/compiler-dom': 3.5.22
'@vue/compiler-ssr': 3.5.22
'@vue/shared': 3.5.22
'@babel/parser': 7.28.5
'@vue/compiler-core': 3.5.24
'@vue/compiler-dom': 3.5.24
'@vue/compiler-ssr': 3.5.24
'@vue/shared': 3.5.24
estree-walker: 2.0.2
magic-string: 0.30.19
magic-string: 0.30.21
postcss: 8.5.6
source-map-js: 1.2.1
@ -8041,10 +8047,10 @@ snapshots:
'@vue/compiler-dom': 3.5.18
'@vue/shared': 3.5.18
'@vue/compiler-ssr@3.5.22':
'@vue/compiler-ssr@3.5.24':
dependencies:
'@vue/compiler-dom': 3.5.22
'@vue/shared': 3.5.22
'@vue/compiler-dom': 3.5.24
'@vue/shared': 3.5.24
'@vue/compiler-vue2@2.7.16':
dependencies:
@ -8053,7 +8059,7 @@ snapshots:
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-core@7.7.7(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))':
'@vue/devtools-core@7.7.7(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@vue/devtools-kit': 7.7.7
'@vue/devtools-shared': 7.7.7
@ -8061,7 +8067,7 @@ snapshots:
nanoid: 5.1.5
pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
transitivePeerDependencies:
- vite
@ -8092,55 +8098,55 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
'@vue/reactivity@3.5.22':
'@vue/reactivity@3.5.24':
dependencies:
'@vue/shared': 3.5.22
'@vue/shared': 3.5.24
'@vue/runtime-core@3.5.22':
'@vue/runtime-core@3.5.24':
dependencies:
'@vue/reactivity': 3.5.22
'@vue/shared': 3.5.22
'@vue/reactivity': 3.5.24
'@vue/shared': 3.5.24
'@vue/runtime-dom@3.5.22':
'@vue/runtime-dom@3.5.24':
dependencies:
'@vue/reactivity': 3.5.22
'@vue/runtime-core': 3.5.22
'@vue/shared': 3.5.22
csstype: 3.1.3
'@vue/reactivity': 3.5.24
'@vue/runtime-core': 3.5.24
'@vue/shared': 3.5.24
csstype: 3.2.0
'@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.8.3))':
'@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@vue/compiler-ssr': 3.5.22
'@vue/shared': 3.5.22
vue: 3.5.22(typescript@5.8.3)
'@vue/compiler-ssr': 3.5.24
'@vue/shared': 3.5.24
vue: 3.5.24(typescript@5.8.3)
'@vue/shared@3.5.18': {}
'@vue/shared@3.5.22': {}
'@vue/shared@3.5.24': {}
'@vueuse/core@13.6.0(vue@3.5.22(typescript@5.8.3))':
'@vueuse/core@13.6.0(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.6.0
'@vueuse/shared': 13.6.0(vue@3.5.22(typescript@5.8.3))
vue: 3.5.22(typescript@5.8.3)
'@vueuse/shared': 13.6.0(vue@3.5.24(typescript@5.8.3))
vue: 3.5.24(typescript@5.8.3)
'@vueuse/metadata@13.6.0': {}
'@vueuse/nuxt@13.6.0(magicast@0.3.5)(nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.22)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))':
'@vueuse/nuxt@13.6.0(magicast@0.3.5)(nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.24)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))':
dependencies:
'@nuxt/kit': 4.0.2(magicast@0.3.5)
'@vueuse/core': 13.6.0(vue@3.5.22(typescript@5.8.3))
'@vueuse/core': 13.6.0(vue@3.5.24(typescript@5.8.3))
'@vueuse/metadata': 13.6.0
local-pkg: 1.1.1
nuxt: 3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.22)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0)
vue: 3.5.22(typescript@5.8.3)
nuxt: 3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.24)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0)
vue: 3.5.24(typescript@5.8.3)
transitivePeerDependencies:
- magicast
'@vueuse/shared@13.6.0(vue@3.5.22(typescript@5.8.3))':
'@vueuse/shared@13.6.0(vue@3.5.24(typescript@5.8.3))':
dependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
'@whatwg-node/disposablestack@0.0.6':
dependencies:
@ -8742,7 +8748,7 @@ snapshots:
dependencies:
css-tree: 2.2.1
csstype@3.1.3: {}
csstype@3.2.0: {}
data-uri-to-buffer@4.0.1: {}
@ -8849,7 +8855,7 @@ snapshots:
detective-vue2@2.2.0(typescript@5.8.3):
dependencies:
'@dependents/detective-less': 5.0.1
'@vue/compiler-sfc': 3.5.22
'@vue/compiler-sfc': 3.5.24
detective-es6: 5.0.1
detective-sass: 6.0.1
detective-scss: 5.0.1
@ -9149,9 +9155,9 @@ snapshots:
optionalDependencies:
'@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.5.1))(typescript@5.8.3)
eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.31.0(jiti@2.5.1)):
eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.24)(eslint@9.31.0(jiti@2.5.1)):
dependencies:
'@vue/compiler-sfc': 3.5.22
'@vue/compiler-sfc': 3.5.24
eslint: 9.31.0(jiti@2.5.1)
eslint-scope@8.4.0:
@ -10051,7 +10057,7 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.4
magic-string@0.30.19:
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -10502,16 +10508,16 @@ snapshots:
- rollup
- supports-color
nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.22)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0):
nuxt@3.17.7(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.24)(db0@0.3.2)(eslint@9.31.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.0):
dependencies:
'@nuxt/cli': 3.27.0(magicast@0.3.5)
'@nuxt/devalue': 2.0.2
'@nuxt/devtools': 2.6.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3))
'@nuxt/devtools': 2.6.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3))
'@nuxt/kit': 3.17.7(magicast@0.3.5)
'@nuxt/schema': 3.17.7
'@nuxt/telemetry': 2.6.6(magicast@0.3.5)
'@nuxt/vite-builder': 3.17.7(@types/node@22.16.5)(eslint@9.31.0(jiti@2.5.1))(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vue-tsc@3.0.4(typescript@5.8.3))(vue@3.5.22(typescript@5.8.3))(yaml@2.8.0)
'@unhead/vue': 2.0.13(vue@3.5.22(typescript@5.8.3))
'@nuxt/vite-builder': 3.17.7(@types/node@22.16.5)(eslint@9.31.0(jiti@2.5.1))(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vue-tsc@3.0.4(typescript@5.8.3))(vue@3.5.24(typescript@5.8.3))(yaml@2.8.0)
'@unhead/vue': 2.0.13(vue@3.5.24(typescript@5.8.3))
'@vue/shared': 3.5.18
c12: 3.2.0(magicast@0.3.5)
chokidar: 4.0.3
@ -10558,13 +10564,13 @@ snapshots:
unctx: 2.4.1
unimport: 5.2.0
unplugin: 2.3.5
unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.22)(vue-router@4.5.1(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3))
unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.24)(vue-router@4.6.3(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3))
unstorage: 1.16.1(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.7.0)
untyped: 2.0.0
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue-bundle-renderer: 2.1.2
vue-devtools-stub: 0.1.0
vue-router: 4.5.1(vue@3.5.22(typescript@5.8.3))
vue-router: 4.6.3(vue@3.5.24(typescript@5.8.3))
optionalDependencies:
'@parcel/watcher': 2.5.1
'@types/node': 22.16.5
@ -11861,10 +11867,10 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
unplugin-vue-router@0.12.0(vue-router@4.5.1(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3)):
unplugin-vue-router@0.12.0(vue-router@4.6.3(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3)):
dependencies:
'@babel/types': 7.28.2
'@vue-macros/common': 1.16.1(vue@3.5.22(typescript@5.8.3))
'@vue-macros/common': 1.16.1(vue@3.5.24(typescript@5.8.3))
ast-walker-scope: 0.6.2
chokidar: 4.0.3
fast-glob: 3.3.3
@ -11879,14 +11885,14 @@ snapshots:
unplugin-utils: 0.2.4
yaml: 2.8.0
optionalDependencies:
vue-router: 4.5.1(vue@3.5.22(typescript@5.8.3))
vue-router: 4.6.3(vue@3.5.24(typescript@5.8.3))
transitivePeerDependencies:
- vue
unplugin-vue-router@0.14.0(@vue/compiler-sfc@3.5.22)(vue-router@4.5.1(vue@3.5.22(typescript@5.8.3)))(vue@3.5.22(typescript@5.8.3)):
unplugin-vue-router@0.14.0(@vue/compiler-sfc@3.5.24)(vue-router@4.6.3(vue@3.5.24(typescript@5.8.3)))(vue@3.5.24(typescript@5.8.3)):
dependencies:
'@vue-macros/common': 3.0.0-beta.15(vue@3.5.22(typescript@5.8.3))
'@vue/compiler-sfc': 3.5.22
'@vue-macros/common': 3.0.0-beta.15(vue@3.5.24(typescript@5.8.3))
'@vue/compiler-sfc': 3.5.24
ast-walker-scope: 0.8.1
chokidar: 4.0.3
fast-glob: 3.3.3
@ -11901,7 +11907,7 @@ snapshots:
unplugin-utils: 0.2.4
yaml: 2.8.0
optionalDependencies:
vue-router: 4.5.1(vue@3.5.22(typescript@5.8.3))
vue-router: 4.6.3(vue@3.5.24(typescript@5.8.3))
transitivePeerDependencies:
- vue
@ -12080,7 +12086,7 @@ snapshots:
tinyglobby: 0.2.14
vite: 7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vite-plugin-vue-tracer@1.0.0(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.22(typescript@5.8.3)):
vite-plugin-vue-tracer@1.0.0(vite@7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.24(typescript@5.8.3)):
dependencies:
estree-walker: 3.0.3
exsolve: 1.0.7
@ -12088,7 +12094,7 @@ snapshots:
pathe: 2.0.3
source-map-js: 1.2.1
vite: 7.0.6(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0):
dependencies:
@ -12144,17 +12150,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-i18n@10.0.8(vue@3.5.22(typescript@5.8.3)):
vue-i18n@10.0.8(vue@3.5.24(typescript@5.8.3)):
dependencies:
'@intlify/core-base': 10.0.8
'@intlify/shared': 10.0.8
'@vue/devtools-api': 6.6.4
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue-router@4.5.1(vue@3.5.22(typescript@5.8.3)):
vue-router@4.6.3(vue@3.5.24(typescript@5.8.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue-tsc@3.0.4(typescript@5.8.3):
dependencies:
@ -12162,36 +12168,36 @@ snapshots:
'@vue/language-core': 3.0.4(typescript@5.8.3)
typescript: 5.8.3
vue3-carousel-nuxt@1.1.6(magicast@0.3.5)(vue@3.5.22(typescript@5.8.3)):
vue3-carousel-nuxt@1.1.6(magicast@0.3.5)(vue@3.5.24(typescript@5.8.3)):
dependencies:
'@nuxt/kit': 3.18.0(magicast@0.3.5)
vue3-carousel: 0.15.1(vue@3.5.22(typescript@5.8.3))
vue3-carousel: 0.15.1(vue@3.5.24(typescript@5.8.3))
transitivePeerDependencies:
- magicast
- vue
vue3-carousel@0.15.1(vue@3.5.22(typescript@5.8.3)):
vue3-carousel@0.15.1(vue@3.5.24(typescript@5.8.3)):
dependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue3-carousel@0.16.0(vue@3.5.22(typescript@5.8.3)):
vue3-carousel@0.16.0(vue@3.5.24(typescript@5.8.3)):
dependencies:
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
vue@3.5.22(typescript@5.8.3):
vue@3.5.24(typescript@5.8.3):
dependencies:
'@vue/compiler-dom': 3.5.22
'@vue/compiler-sfc': 3.5.22
'@vue/runtime-dom': 3.5.22
'@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.8.3))
'@vue/shared': 3.5.22
'@vue/compiler-dom': 3.5.24
'@vue/compiler-sfc': 3.5.24
'@vue/runtime-dom': 3.5.24
'@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.8.3))
'@vue/shared': 3.5.24
optionalDependencies:
typescript: 5.8.3
vuedraggable@4.1.0(vue@3.5.22(typescript@5.8.3)):
vuedraggable@4.1.0(vue@3.5.24(typescript@5.8.3)):
dependencies:
sortablejs: 1.14.0
vue: 3.5.22(typescript@5.8.3)
vue: 3.5.24(typescript@5.8.3)
web-streams-polyfill@3.3.3: {}

View File

@ -1,5 +1,5 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]);
@ -7,11 +7,7 @@ export default defineEventHandler(async (h3) => {
const gameId = getRouterParam(h3, "id")!;
await prisma.game.delete({
where: {
id: gameId,
},
});
libraryManager.deleteGame(gameId);
return {};
});

View File

@ -1,3 +1,4 @@
import type { GameVersion } from "~/prisma/client/client";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
@ -28,10 +29,22 @@ export default defineEventHandler(async (h3) => {
if (!game || !game.libraryId)
throw createError({ statusCode: 404, statusMessage: "Game ID not found" });
const getGameVersionSize = async (version: GameVersion) => {
const size = await libraryManager.getGameVersionSize(
gameId,
version.versionName,
);
return { ...version, size };
};
const gameWithVersionSize = {
...game,
versions: await Promise.all(game.versions.map(getGameVersionSize)),
};
const unimportedVersions = await libraryManager.fetchUnimportedGameVersions(
game.libraryId,
game.libraryPath,
);
return { game, unimportedVersions };
return { game: gameWithVersionSize, unimportedVersions };
});

View File

@ -1,7 +1,7 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
const DeleteVersion = type({
id: "string",
@ -20,15 +20,7 @@ export default defineEventHandler<{ body: typeof DeleteVersion }>(
const gameId = body.id.toString();
const version = body.versionName.toString();
await prisma.gameVersion.delete({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
},
});
await libraryManager.deleteGameVersion(gameId, version);
return {};
},
);

View File

@ -0,0 +1,27 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { systemConfig } from "~/server/internal/config/sys-conf";
import libraryManager from "~/server/internal/library";
import userStatsManager from "~/server/internal/userstats";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const sources = await libraryManager.fetchLibraries();
const userStats = await userStatsManager.getUserStats();
const biggestGamesCombined =
await libraryManager.getBiggestGamesCombinedVersions(5);
const biggestGamesLatest =
await libraryManager.getBiggestGamesLatestVersions(5);
return {
gameCount: await prisma.game.count(),
version: systemConfig.getDropVersion(),
userStats,
sources,
biggestGamesLatest,
biggestGamesCombined,
};
});

View File

@ -2,7 +2,10 @@ import type { LibraryModel } from "~/prisma/client/models";
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
export type WorkingLibrarySource = LibraryModel & { working: boolean };
export type WorkingLibrarySource = LibraryModel & {
working: boolean;
fsStats?: { freeSpace: number; totalSpace: number } | undefined;
};
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [

View File

@ -3,8 +3,8 @@ import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
import { libraryConstructors } from "~/server/plugins/05.library-init";
import type { WorkingLibrarySource } from "./index.get";
const UpdateLibrarySource = type({
id: "string",
@ -49,8 +49,8 @@ export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>(
},
});
await libraryManager.removeLibrary(source.id);
await libraryManager.addLibrary(newLibrary);
libraryManager.removeLibrary(source.id);
libraryManager.addLibrary(newLibrary);
const workingSource: WorkingLibrarySource = {
...updatedSource,

View File

@ -6,7 +6,7 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
import { libraryConstructors } from "~/server/plugins/05.library-init";
import type { WorkingLibrarySource } from "./index.get";
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
const CreateLibrarySource = type({
name: "string",
@ -52,11 +52,12 @@ export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>(
},
});
await libraryManager.addLibrary(library);
libraryManager.addLibrary(library);
const workingSource: WorkingLibrarySource = {
...source,
working: true,
fsStats: library.fsStats(),
};
return workingSource;

View File

@ -1,6 +1,7 @@
import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import userStatsManager from "~/server/internal/userstats";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["user:delete"]);
@ -27,5 +28,6 @@ export default defineEventHandler(async (h3) => {
throw createError({ statusCode: 404, statusMessage: "User not found." });
await prisma.user.delete({ where: { id: userId } });
await userStatsManager.deleteUser();
return { success: true };
});

View File

@ -6,6 +6,7 @@ import objectHandler from "~/server/internal/objects";
import { type } from "arktype";
import { randomUUID } from "node:crypto";
import { throwingArktype } from "~/server/arktype";
import userStatsManager from "~/server/internal/userstats";
export const SharedRegisterValidator = type({
username: "string >= 5",
@ -86,5 +87,6 @@ export default defineEventHandler<{
prisma.invitation.delete({ where: { id: user.invitation } }),
]);
await userStatsManager.addUser();
return linkMec.user;
});

View File

@ -1,5 +1,6 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3);
@ -26,5 +27,8 @@ export default defineClientEventHandler(async (h3) => {
statusMessage: "Game version not found",
});
return gameVersion;
return {
...gameVersion,
size: libraryManager.getGameVersionSize(id, version),
};
});

View File

@ -1,5 +1,6 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
@ -51,5 +52,7 @@ export default defineEventHandler(async (h3) => {
},
});
return { game, rating };
const size = await libraryManager.getGameVersionSize(game.id);
return { game, rating, size };
});

View File

@ -8,6 +8,7 @@ import type {
} from "./capabilities";
import capabilityManager from "./capabilities";
import type { PeerImpl } from "../tasks";
import userStatsManager from "~/server/internal/userstats";
export enum AuthMode {
Callback = "callback",
@ -136,7 +137,7 @@ export class ClientHandler {
statusCode: 400,
statusMessage: "Client has not connected yet. Please try again later.",
});
await client.peer.send(
client.peer.send(
JSON.stringify({ type: "token", value: `${clientId}/${token}` }),
);
}
@ -166,6 +167,7 @@ export class ClientHandler {
lastConnected: new Date(),
},
});
await userStatsManager.cacheUserSessions();
for (const [capability, configuration] of Object.entries(
metadata.data.capabilities,
@ -191,6 +193,7 @@ export class ClientHandler {
id,
},
});
await userStatsManager.cacheUserStats();
}
}

View File

@ -1,5 +1,6 @@
import type { GameVersionModel } from "~/prisma/client/models";
import prisma from "../db/database";
import { sum } from "~/utils/array";
export type DropChunk = {
permissions: number;
@ -102,6 +103,14 @@ class ManifestGenerator {
return manifest;
}
calculateManifestSize(manifest: DropManifest) {
return sum(
Object.values(manifest)
.map((chunk) => chunk.lengths)
.flat(),
);
}
}
export const manifestGenerator = new ManifestGenerator();

View File

@ -0,0 +1,236 @@
import cacheHandler from "../cache";
import prisma from "../db/database";
import manifestGenerator from "../downloads/manifest";
import { sum } from "../../../utils/array";
import type { Game, GameVersion } from "~/prisma/client/client";
export type GameSize = {
gameName: string;
size: number;
gameId: string;
};
export type VersionSize = GameSize & {
latest: boolean;
};
type VersionsSizes = {
[versionName: string]: VersionSize;
};
type GameVersionsSize = {
[gameId: string]: VersionsSizes;
};
class GameSizeManager {
private gameVersionsSizesCache =
cacheHandler.createCache<GameVersionsSize>("gameVersionsSizes");
// All versions sizes combined
private gameSizesCache = cacheHandler.createCache<GameSize>("gameSizes");
private async clearGameVersionsSizesCache() {
(await this.gameVersionsSizesCache.getKeys()).map((key) =>
this.gameVersionsSizesCache.remove(key),
);
}
private async clearGameSizesCache() {
(await this.gameSizesCache.getKeys()).map((key) =>
this.gameSizesCache.remove(key),
);
}
// All versions of a game combined
async getCombinedGameSize(gameId: string) {
const versions = await prisma.gameVersion.findMany({
where: { gameId },
});
const sizes = await Promise.all(
versions.map((version) =>
manifestGenerator.calculateManifestSize(
JSON.parse(version.dropletManifest as string),
),
),
);
return sum(sizes);
}
async getGameVersionSize(
gameId: string,
versionName?: string,
): Promise<number | null> {
if (!versionName) {
const version = await prisma.gameVersion.findFirst({
where: { gameId },
orderBy: {
versionIndex: "desc",
},
});
if (!version) {
return null;
}
versionName = version.versionName;
}
const manifest = await manifestGenerator.generateManifest(
gameId,
versionName,
);
if (!manifest) {
return null;
}
return manifestGenerator.calculateManifestSize(manifest);
}
private async isLatestVersion(
gameVersions: GameVersion[],
version: GameVersion,
): Promise<boolean> {
return gameVersions.length > 0
? gameVersions[0].versionName === version.versionName
: false;
}
async getBiggestGamesLatestVersion(top: number): Promise<VersionSize[]> {
const gameIds = await this.gameVersionsSizesCache.getKeys();
const latestGames = await Promise.all(
gameIds.map(async (gameId) => {
const versionsSizes = await this.gameVersionsSizesCache.get(gameId);
if (!versionsSizes) {
return null;
}
const latestVersionName = Object.keys(versionsSizes).find(
(versionName) => versionsSizes[versionName].latest,
);
if (!latestVersionName) {
return null;
}
return versionsSizes[latestVersionName] || null;
}),
);
return latestGames
.filter((game) => game !== null)
.sort((gameA, gameB) => gameB.size - gameA.size)
.slice(0, top);
}
async isGameVersionsSizesCacheEmpty() {
return (await this.gameVersionsSizesCache.getKeys()).length === 0;
}
async isGameSizesCacheEmpty() {
return (await this.gameSizesCache.getKeys()).length === 0;
}
async cacheAllCombinedGames() {
await this.clearGameSizesCache();
const games = await prisma.game.findMany({ include: { versions: true } });
await Promise.all(games.map((game) => this.cacheCombinedGame(game)));
}
async cacheCombinedGame(game: Game) {
const size = await this.getCombinedGameSize(game.id);
if (!size) {
this.gameSizesCache.remove(game.id);
return;
}
const gameSize = {
size,
gameName: game.mName,
gameId: game.id,
};
await this.gameSizesCache.set(game.id, gameSize);
}
async cacheAllGameVersions() {
await this.clearGameVersionsSizesCache();
const games = await prisma.game.findMany({
include: {
versions: {
orderBy: {
versionIndex: "desc",
},
take: 1,
},
},
});
await Promise.all(games.map((game) => this.cacheGameVersion(game)));
}
async cacheGameVersion(
game: Game & { versions: GameVersion[] },
versionName?: string,
) {
const cacheVersion = async (version: GameVersion) => {
const size = await this.getGameVersionSize(game.id, version.versionName);
if (!version.versionName || !size) {
return;
}
const versionsSizes = {
[version.versionName]: {
size,
gameName: game.mName,
gameId: game.id,
latest: await this.isLatestVersion(game.versions, version),
},
};
const allVersionsSizes =
(await this.gameVersionsSizesCache.get(game.id)) || {};
await this.gameVersionsSizesCache.set(game.id, {
...allVersionsSizes,
...versionsSizes,
});
};
if (versionName) {
const version = await prisma.gameVersion.findFirst({
where: { gameId: game.id, versionName },
});
if (!version) {
return;
}
cacheVersion(version);
return;
}
if ("versions" in game) {
await Promise.all(game.versions.map(cacheVersion));
}
}
async getBiggestGamesAllVersions(top: number): Promise<GameSize[]> {
const gameIds = await this.gameSizesCache.getKeys();
const allGames = await Promise.all(
gameIds.map(async (gameId) => await this.gameSizesCache.get(gameId)),
);
return allGames
.filter((game) => game !== null)
.sort((gameA, gameB) => gameB.size - gameA.size)
.slice(0, top);
}
async deleteGameVersion(gameId: string, version: string) {
const game = await prisma.game.findFirst({ where: { id: gameId } });
if (game) {
await this.cacheCombinedGame(game);
}
const versionsSizes = await this.gameVersionsSizesCache.get(gameId);
if (!versionsSizes) {
return;
}
// Remove the version from the VersionsSizes object
const { [version]: _, ...updatedVersionsSizes } = versionsSizes;
await this.gameVersionsSizesCache.set(gameId, updatedVersionsSizes);
}
async deleteGame(gameId: string) {
this.gameSizesCache.remove(gameId);
this.gameVersionsSizesCache.remove(gameId);
}
}
export const manager = new GameSizeManager();
export default manager;

View File

@ -15,6 +15,8 @@ import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import type { GameModel } from "~/prisma/client/models";
import { createHash } from "node:crypto";
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
import gameSizeManager from "~/server/internal/gamesize";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@ -39,13 +41,19 @@ class LibraryManager {
this.libraries.delete(id);
}
async fetchLibraries() {
async fetchLibraries(): Promise<WorkingLibrarySource[]> {
const libraries = await prisma.library.findMany({});
const libraryWithMetadata = libraries.map((e) => ({
...e,
working: this.libraries.has(e.id),
}));
return libraryWithMetadata;
const libraryWithMetadata = libraries.map(async (library) => {
const theLibrary = this.libraries.get(library.id);
const working = this.libraries.has(library.id);
return {
...library,
working,
fsStats: working ? theLibrary?.fsStats() : undefined,
};
});
return await Promise.all(libraryWithMetadata);
}
async fetchGamesByLibrary() {
@ -334,6 +342,8 @@ class LibraryManager {
acls: ["system:import:version:read"],
});
await libraryManager.cacheCombinedGameSize(gameId);
await libraryManager.cacheGameVersionSize(gameId, versionName);
progress(100);
},
});
@ -363,6 +373,68 @@ class LibraryManager {
if (!library) return undefined;
return await library.readFile(game, version, filename, options);
}
async deleteGameVersion(gameId: string, version: string) {
await prisma.gameVersion.delete({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
},
});
await gameSizeManager.deleteGameVersion(gameId, version);
}
async deleteGame(gameId: string) {
await prisma.game.delete({
where: {
id: gameId,
},
});
gameSizeManager.deleteGame(gameId);
}
async getGameVersionSize(
gameId: string,
versionName?: string,
): Promise<number | null> {
return gameSizeManager.getGameVersionSize(gameId, versionName);
}
async getBiggestGamesCombinedVersions(top: number) {
if (await gameSizeManager.isGameSizesCacheEmpty()) {
await gameSizeManager.cacheAllCombinedGames();
}
return gameSizeManager.getBiggestGamesAllVersions(top);
}
async getBiggestGamesLatestVersions(top: number) {
if (await gameSizeManager.isGameVersionsSizesCacheEmpty()) {
await gameSizeManager.cacheAllGameVersions();
}
return gameSizeManager.getBiggestGamesLatestVersion(top);
}
async cacheCombinedGameSize(gameId: string) {
const game = await prisma.game.findFirst({ where: { id: gameId } });
if (!game) {
return;
}
await gameSizeManager.cacheCombinedGame(game);
}
async cacheGameVersionSize(gameId: string, versionName: string) {
const game = await prisma.game.findFirst({
where: { id: gameId },
include: { versions: true },
});
if (!game) {
return;
}
await gameSizeManager.cacheGameVersion(game, versionName);
}
}
export const libraryManager = new LibraryManager();

View File

@ -57,6 +57,8 @@ export abstract class LibraryProvider<CFG> {
filename: string,
options?: { start?: number; end?: number },
): Promise<ReadableStream | undefined>;
abstract fsStats(): { freeSpace: number; totalSpace: number } | undefined;
}
export class GameNotFoundError extends Error {}

View File

@ -8,6 +8,7 @@ import { LibraryBackend } from "~/prisma/client/enums";
import fs from "fs";
import path from "path";
import droplet, { DropletHandler } from "@drop-oss/droplet";
import { fsStats } from "~/server/internal/utils/files";
export const FilesystemProviderConfig = type({
baseDir: "string",
@ -122,4 +123,8 @@ export class FilesystemProvider
return stream;
}
fsStats() {
return fsStats(this.config.baseDir);
}
}

View File

@ -6,6 +6,7 @@ import fs from "fs";
import path from "path";
import droplet from "@drop-oss/droplet";
import { DROPLET_HANDLER } from "./filesystem";
import { fsStats } from "~/server/internal/utils/files";
export const FlatFilesystemProviderConfig = type({
baseDir: "string",
@ -113,4 +114,8 @@ export class FlatFilesystemProvider
return stream.getStream();
}
fsStats() {
return fsStats(this.config.baseDir);
}
}

View File

@ -0,0 +1,68 @@
/*
Handles managing collections
*/
import cacheHandler from "../cache";
import prisma from "../db/database";
import { DateTime } from "luxon";
class UserStatsManager {
// Caches the user's core library
private userStatsCache = cacheHandler.createCache<number>("userStats");
async cacheUserSessions() {
const activeSessions =
(
await prisma.client.groupBy({
by: ["userId"],
where: {
id: { not: "system" },
lastConnected: {
gt: DateTime.now().minus({ months: 1 }).toISO(),
},
},
})
).length || 0;
await this.userStatsCache.set("activeSessions", activeSessions);
}
private async cacheUserCount() {
const userCount =
(await prisma.user.count({
where: { id: { not: "system" } },
})) || 0;
await this.userStatsCache.set("userCount", userCount);
}
async cacheUserStats() {
await this.cacheUserSessions();
await this.cacheUserCount();
}
async getUserStats() {
let activeSessions = await this.userStatsCache.get("activeSessions");
let userCount = await this.userStatsCache.get("userCount");
if (activeSessions === null || userCount === null) {
await this.cacheUserStats();
activeSessions = (await this.userStatsCache.get("activeSessions")) || 0;
userCount = (await this.userStatsCache.get("userCount")) || 0;
}
return { activeSessions, userCount };
}
async addUser() {
const userCount = (await this.userStatsCache.get("userCount")) || 0;
await this.userStatsCache.set("userCount", userCount + 1);
}
async deleteUser() {
const userCount = (await this.userStatsCache.get("userCount")) || 1;
await this.userStatsCache.set("userCount", userCount - 1);
await this.cacheUserSessions();
}
}
export const manager = new UserStatsManager();
export default manager;

View File

@ -0,0 +1,47 @@
import fs from "fs";
import nodePath from "path";
export function fsStats(folderPath: string) {
const stats = fs.statfsSync(folderPath);
const freeSpace = stats.bavail * stats.bsize;
const totalSpace = stats.blocks * stats.bsize;
return { freeSpace, totalSpace };
}
export function getFolderSize(folderPath: string): number {
const files = fs.readdirSync(folderPath, { withFileTypes: true });
const paths = files.map((file) => {
const path = nodePath.join(folderPath, file.name);
if (file.isDirectory()) {
return getFolderSize(path);
}
if (file.isFile()) {
return fs.statSync(path).size;
}
return 0;
});
return paths
.flat(Infinity)
.reduce(
(accumulator: number, currentValue: number) => accumulator + currentValue,
0,
);
}
export function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes >= 1024 && bytes < Math.pow(1024, 2)) {
return `${(bytes / 1024).toFixed(2)} KiB`;
}
if (bytes >= Math.pow(1024, 2) && bytes < Math.pow(1024, 3)) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
}
if (bytes >= Math.pow(1024, 3) && bytes < Math.pow(1024, 4)) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
}
return `${(bytes / Math.pow(1024, 4)).toFixed(2)} TiB`;
}

6
utils/array.ts Normal file
View File

@ -0,0 +1,6 @@
export const sum = (array: number[]) =>
array.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
export function lastItem<T>(array: T[]) {
return array[array.length - 1];
}

76
utils/colors.ts Normal file
View File

@ -0,0 +1,76 @@
export const CHART_COLOURS = {
// Bar colours
red: {
fill: "fill-red-700",
bg: "bg-red-700",
},
orange: {
fill: "fill-orange-800",
bg: "bg-orange-800",
},
blue: {
fill: "fill-blue-900",
bg: "bg-blue-900",
},
// Pie colours
lightblue: {
fill: "fill-blue-400",
bg: "bg-blue-400",
},
dropblue: {
fill: "fill-blue-600",
bg: "bg-blue-600",
},
green: {
fill: "fill-green-500",
bg: "bg-green-500",
},
yellow: {
fill: "fill-yellow-800",
bg: "bg-yellow-800",
},
purple: {
fill: "fill-purple-500",
bg: "bg-purple-500",
},
zinc: {
fill: "fill-zinc-950",
bg: "bg-zinc-950",
},
pink: {
fill: "fill-pink-800",
bg: "bg-pink-800",
},
lime: {
fill: "fill-lime-600",
bg: "bg-lime-600",
},
emerald: {
fill: "fill-emerald-500",
bg: "bg-emerald-500",
},
slate: {
fill: "fill-slate-800",
bg: "bg-slate-800",
},
};
export const PIE_COLOURS: ChartColour[] = [
"lightblue",
"dropblue",
"purple",
"emerald",
];
export type ChartColour = keyof typeof CHART_COLOURS;
export function getBarColor(percentage: number): ChartColour {
if (percentage <= 70) {
return "blue";
}
if (percentage > 70 && percentage <= 90) {
return "orange";
}
return "red";
}

13
utils/tuple.ts Normal file
View File

@ -0,0 +1,13 @@
export default class Tuple {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
toString() {
return `${this.x},${this.y}`;
}
}