From 6e8edbf4ca0824c1025ca3765c031cf7f9c7627f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 16 Feb 2026 15:34:22 +1100 Subject: [PATCH 1/5] feat: new page layout + endpoint --- .vscode/settings.json | 1 - package.json | 2 + pages/admin/library/import.vue | 6 + pages/admin/library/index.vue | 138 ++++--- pages/admin/library/mass-import.vue | 345 ++++++++++++++++++ pages/admin/task/index.vue | 57 ++- pnpm-lock.yaml | 147 ++++++++ .../v1/admin/import/massversion/index.get.ts | 54 +++ .../v1/admin/import/massversion/index.post.ts | 101 +++++ .../api/v1/admin/import/version/index.post.ts | 40 -- server/api/v1/admin/library/index.get.ts | 38 +- server/api/v1/admin/library/libraries.get.ts | 12 + server/api/v1/admin/task/index.get.ts | 14 +- server/internal/library/index.ts | 130 +++++-- server/internal/services/index.ts | 9 +- server/internal/tasks/group.ts | 3 + 16 files changed, 959 insertions(+), 138 deletions(-) create mode 100644 pages/admin/library/mass-import.vue create mode 100644 server/api/v1/admin/import/massversion/index.get.ts create mode 100644 server/api/v1/admin/import/massversion/index.post.ts create mode 100644 server/api/v1/admin/library/libraries.get.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 0570fe39..4769d729 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,6 @@ "i18n-ally.extract.ignored": [ "string >= 14", "string.alphanumeric >= 5", - "/api/v1/admin/import/version/preload?id=${encodeURIComponent(\n gameId,\n )}&version=${encodeURIComponent(version)}" ], "i18n-ally.extract.ignoredByFiles": { "components/NewsArticleCreateButton.vue": ["[", "`", "Enter"], diff --git a/package.json b/package.json index 10114bb0..00c8818b 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@bufbuild/buf": "^1.65.0", "@bufbuild/protoc-gen-es": "^2.11.0", + "@golar/vue": "^0.0.13", "@intlify/eslint-plugin-vue-i18n": "^4.0.1", "@nuxt/eslint": "^1.3.0", "@tailwindcss/forms": "^0.5.9", @@ -86,6 +87,7 @@ "autoprefixer": "^10.4.20", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", + "golar": "^0.0.13", "h3": "^1.15.5", "nitropack": "^2.11.12", "ofetch": "^1.4.1", diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue index cce2dd80..2caa48de 100644 --- a/pages/admin/library/import.vue +++ b/pages/admin/library/import.vue @@ -344,6 +344,7 @@ import { import { XCircleIcon } from "@heroicons/vue/16/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline"; +import { FetchError } from "ofetch"; import type { GameType } from "~/prisma/client/enums"; import type { GameMetadataSearchResult } from "~/server/internal/metadata/types"; @@ -406,6 +407,11 @@ async function searchGame() { gameSearchLoading.value = false; } catch (e) { gameSearchLoading.value = false; + if (e instanceof FetchError) { + gameSearchResultsError.value = e.data?.message ?? t("errors.unknown"); + } else { + gameSearchResultsError.value = (e as string)?.toString(); + } throw e; } diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index 02a0bc9e..129cdb58 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -26,34 +26,66 @@ -
-
-
-
-
    +
    • {{ $t("tasks.admin.completedTasksTitle") }} -
        +
        • { }); } - const versionImportTaskId = await libraryManager.importVersion( + logger.info(`importing ${version.version.name}`); + const min = versionIndex / body.versions.length; + const max = (versionIndex + 1) / body.versions.length; + + await libraryManager.importVersion( version.id, version.version, { @@ -86,13 +90,23 @@ export default defineEventHandler(async (h3) => { delta: false, requiredContent: [], }, + wrapTaskContext( + { + logger, + progress, + addAction, + }, + { + min: min * 100, + max: max * 100, + prefix: `${version.version.name}`, + }, + ), ); - addAction( - `View ${version.version.name} import:/admin/task/${versionImportTaskId}`, - ); + logger.info(`finished import for ${version.version.name}`); - progress((versionIndex / body.versions.length) * 100); + progress(max * 100); } }, }); diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index f0941ec7..765311a2 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -8,6 +8,7 @@ import path from "path"; import prisma from "../db/database"; import { fuzzy } from "fast-fuzzy"; +import type { TaskRunContext } from "../tasks"; import taskHandler from "../tasks"; import notificationSystem from "../notifications"; import { GameNotFoundError, type LibraryProvider } from "./provider"; @@ -398,6 +399,7 @@ class LibraryManager { gameId: string, version: UnimportedVersionInformation, metadata: typeof ImportVersion.infer, + parentTask?: TaskRunContext, ) { const taskKey = createVersionImportTaskKey(gameId, version.identifier); @@ -581,7 +583,7 @@ class LibraryManager { } progress(100); }, - }); + }, parentTask); } async peekFile( diff --git a/server/internal/tasks/index.ts b/server/internal/tasks/index.ts index 189dac09..8a47483f 100644 --- a/server/internal/tasks/index.ts +++ b/server/internal/tasks/index.ts @@ -8,7 +8,7 @@ import checkUpdate from "./registry/update"; import cleanupObjects from "./registry/objects"; import { taskGroups, type TaskGroup } from "./group"; import prisma from "../db/database"; -import { type } from "arktype"; +import { ArkErrors, type } from "arktype"; import pino from "pino"; import { logger } from "~/server/internal/logging"; import { Writable } from "node:stream"; @@ -76,7 +76,7 @@ class TaskHandler { this.taskCreators.set(task.taskGroup, task.build); } - async create(iTask: Omit) { + async create(iTask: Omit, parentTask?: TaskRunContext) { const task: Task = { ...iTask, id: crypto.randomUUID() }; if (this.hasTaskID(task.id)) throw new Error("Task with ID already exists."); @@ -105,6 +105,7 @@ class TaskHandler { const updateAllClients = (reset = false) => new Promise((r) => { + //if (parentTask) return; // NO-OP if we're a child task if (updateCollectTimeout) { updateCollectResolves.push(r); return; @@ -148,7 +149,10 @@ class TaskHandler { write(chunk, encoding, callback) { try { // chunk is a stringified JSON log line - const logObj = JSON.parse(chunk.toString()); + const logObj = TaskLog(JSON.parse(chunk.toString())); + if (logObj instanceof ArkErrors) { + throw logObj; + } const taskEntry = taskPool.get(task.id); if (taskEntry) { taskEntry.log.push(JSON.stringify(logObj)); @@ -156,43 +160,44 @@ class TaskHandler { } } catch (e) { // fallback: ignore or log error - logger.error("Failed to parse log chunk", { - error: e, - chunk: chunk, - }); + logger.error(`Failed to parse log chunk: ${e}, ${chunk}`); } callback(); }, }); // Use pino with the custom stream - const taskLogger = pino( - { - // You can configure timestamp, level, etc. here - timestamp: pino.stdTimeFunctions.isoTime, - base: null, // Remove pid/hostname if not needed - formatters: { - level(label) { - return { - level: label, - }; + const taskLogger = + parentTask?.logger ?? + pino( + { + // You can configure timestamp, level, etc. here + timestamp: pino.stdTimeFunctions.isoTime, + base: null, // Remove pid/hostname if not needed + formatters: { + level(label) { + return { + level: label, + }; + }, }, }, - }, - logStream, - ); + logStream, + ); - const progress = (progress: number) => { - if (progress < 0 || progress > 100) { - logger.error("Progress must be between 0 and 100", { progress }); - return; - } - const taskEntry = this.taskPool.get(task.id); - if (!taskEntry) return; - taskEntry.progress = progress; - // log(`Progress: ${progress}%`); - updateAllClients(); - }; + const progress = + parentTask?.progress ?? + ((progress: number) => { + if (progress < 0 || progress > 100) { + logger.error("Progress must be between 0 and 100", { progress }); + return; + } + const taskEntry = this.taskPool.get(task.id); + if (!taskEntry) return; + taskEntry.progress = progress; + // log(`Progress: ${progress}%`); + updateAllClients(); + }); this.taskPool.set(task.id, { name: task.name, @@ -233,35 +238,37 @@ class TaskHandler { taskEntry.endTime = new Date().toISOString(); await updateAllClients(); - for (const clientId of taskEntry.clients.keys()) { - if (!this.clientRegistry.get(clientId)) continue; - this.disconnect(clientId, task.id); - } - - await prisma.task.create({ - data: { - id: task.id, - taskGroup: taskEntry.taskGroup, - name: taskEntry.name, + if (!parentTask) { + for (const clientId of taskEntry.clients.keys()) { + if (!this.clientRegistry.get(clientId)) continue; + this.disconnect(clientId, task.id); + } - started: taskEntry.startTime, - ended: taskEntry.endTime, + await prisma.task.create({ + data: { + id: task.id, + taskGroup: taskEntry.taskGroup, + name: taskEntry.name, - success: taskEntry.success, - progress: taskEntry.progress, - log: taskEntry.log, + started: taskEntry.startTime, + ended: taskEntry.endTime, - acls: taskEntry.acls, - actions: taskEntry.actions, + success: taskEntry.success, + progress: taskEntry.progress, + log: taskEntry.log, - ...(taskEntry.error ? { error: taskEntry.error } : undefined), - }, - }); + acls: taskEntry.acls, + actions: taskEntry.actions, + ...(taskEntry.error ? { error: taskEntry.error } : undefined), + }, + }); + } this.taskPool.delete(task.id); }; - taskFunc(); + const fnPromise = taskFunc(); + if (parentTask) await fnPromise; return task.id; } @@ -511,9 +518,10 @@ interface DropTask { } export const TaskLog = type({ - timestamp: "string", - message: "string", + time: "string", + msg: "string", level: "string", + prefix: "string?", }); // /** diff --git a/utils/parseTaskLog.ts b/utils/parseTaskLog.ts index 93f1e21f..7c02bf16 100644 --- a/utils/parseTaskLog.ts +++ b/utils/parseTaskLog.ts @@ -14,7 +14,7 @@ const labelNumberMap = { export function parseTaskLog( logStr?: string | undefined, ): typeof TaskLog.infer { - if (!logStr) return { message: "", timestamp: "", level: "" }; + if (!logStr) return { msg: "", time: "", level: "" }; const log = JSON.parse(logStr); if (typeof log.level === "number") { @@ -23,9 +23,5 @@ export function parseTaskLog( ] as string; } - return { - message: log.msg, - timestamp: log.time, - level: log.level, - }; + return log; } From 89ccf4ee776183cda776dcee5f8180bfd2234cf8 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 16 Feb 2026 18:09:46 +1100 Subject: [PATCH 3/5] feat: paginated admin library --- pages/admin/library/index.vue | 115 ++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 6 deletions(-) diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index 129cdb58..5ff6806e 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -95,7 +95,7 @@ v-model="searchQuery" type="text" name="search" - class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6" + class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 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:pl-9 sm:text-sm/6" :placeholder="$t('library.search')" />
          • + +
            +
            + + {{ $t("common.srLoading") }} +
            +
          +
@@ -326,6 +396,8 @@ import { InformationCircleIcon, StarIcon, WrenchScrewdriverIcon, + ArrowLongLeftIcon, + ArrowLongRightIcon, } from "@heroicons/vue/20/solid"; import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline"; import type { AdminLibraryGame } from "~/server/api/v1/admin/library/index.get"; @@ -346,30 +418,61 @@ const { unimportedGames, hasLibraries } = await $dropFetch( "/api/v1/admin/library/libraries", ); +const route = useRoute(); +const router = useRouter(); + // Hard limit on server const pageSize = 12; -const currentIndex = ref(0); +const currentIndex = ref(route.query.page ? parseInt(route.query.page.toString()) - 1 : 0); const maxIndex = ref(0); -const maxPages = computed(() => maxIndex.value / pageSize); +const maxPages = computed(() => Math.ceil(maxIndex.value / pageSize)); const games = ref([]); +const gamesLoading = ref(false); async function fetchPage() { + gamesLoading.value = true; const { results, count } = await $dropFetch("/api/v1/admin/library", { query: { - s: currentIndex.value, + s: currentIndex.value * pageSize, l: pageSize, }, + failTitle: "Failed to fetch game library", }); maxIndex.value = count; games.value = results; + gamesLoading.value = false; + router.push({ + path: route.path, + query: { + ...route.query, + page: currentIndex.value + 1, + }, + }); +} + +function nextPage() { + if (currentIndex.value < maxPages.value - 1) { + currentIndex.value++; + } +} + +function previousPage() { + if (currentIndex.value > 0) { + currentIndex.value--; + } } await fetchPage(); +watch(currentIndex, () => { + fetchPage(); + document.body.scrollTop = document.documentElement.scrollTop = 0; +}); + const toImport = ref(Object.values(unimportedGames).flat().length > 0); -const libraryGames = ref( +const libraryGames = computed(() => games.value.map((e) => { if (e.status == "offline") { return { From 54980f86a08459794ba9695be203b125bb0253eb Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 16 Feb 2026 18:14:15 +1100 Subject: [PATCH 4/5] feat: lint and performance improvement --- .vscode/settings.json | 7 +- components/LogLine.vue | 4 +- pages/admin/library/index.vue | 8 +- pnpm-workspace.yaml | 10 +- server/api/v1/admin/library/index.get.ts | 2 +- server/internal/library/index.ts | 231 ++++++++++++----------- 6 files changed, 135 insertions(+), 127 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4769d729..4a935af9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,10 +4,7 @@ "strings": "on" }, "i18n-ally.extract.autoDetect": true, - "i18n-ally.extract.ignored": [ - "string >= 14", - "string.alphanumeric >= 5", - ], + "i18n-ally.extract.ignored": ["string >= 14", "string.alphanumeric >= 5"], "i18n-ally.extract.ignoredByFiles": { "components/NewsArticleCreateButton.vue": ["[", "`", "Enter"], "pages/admin/library/sources/index.vue": ["Filesystem"], @@ -32,7 +29,7 @@ "username": "drop" } ], - "typescript.experimental.useTsgo": false, + "typescript.experimental.useTsgo": true, // prioritize ArkType's "type" for autoimports "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"] } diff --git a/components/LogLine.vue b/components/LogLine.vue index 1947f24b..8c5d7558 100644 --- a/components/LogLine.vue +++ b/components/LogLine.vue @@ -9,9 +9,7 @@ >{{ log.level }} {{ log.prefix }} -
{{
-      log.msg
-    }}
+
{{ log.msg }}
diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index 5ff6806e..376bcb59 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -315,7 +315,7 @@
Math.ceil(maxIndex.value / pageSize)); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9e33397..9f2dfb72 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,9 @@ onlyBuiltDependencies: - - '@bufbuild/buf' - - '@parcel/watcher' - - '@prisma/client' - - '@prisma/engines' - - '@tailwindcss/oxide' + - "@bufbuild/buf" + - "@parcel/watcher" + - "@prisma/client" + - "@prisma/engines" + - "@tailwindcss/oxide" - argon2 - esbuild - prisma diff --git a/server/api/v1/admin/library/index.get.ts b/server/api/v1/admin/library/index.get.ts index db621698..d07fe9f1 100644 --- a/server/api/v1/admin/library/index.get.ts +++ b/server/api/v1/admin/library/index.get.ts @@ -30,7 +30,7 @@ export default defineEventHandler(async (h3) => { } satisfies FetchArg) : undefined; - const limit = Math.min(query.l ?? 20, 20); + const limit = Math.min(query.l ?? 24, 50); const results = await libraryManager.fetchGamesWithStatus({ ...skip, diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index 765311a2..ffe62016 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -220,6 +220,7 @@ class LibraryManager { include: { library: true, versions: true, + unimportedGameVersions: true, }, }); @@ -228,6 +229,13 @@ class LibraryManager { const unimportedVersions = await this.fetchUnimportedGameVersions( e.libraryId ?? "", e.libraryPath, + { + gameId: e.id, + versions: e.versions + .map((v) => v.versionPath) + .filter((v) => v !== null), + depotVersions: e.unimportedGameVersions, + }, ); return { game: e, @@ -466,124 +474,127 @@ class LibraryManager { }) : undefined; - return await taskHandler.create({ - key: taskKey, - taskGroup: "import:version", - name: `Importing version ${version.name} for ${game.mName}`, - acls: ["system:import:version:read"], - async run({ progress, logger }) { - let versionPath: string | null = null; - let manifest; - let fileList; - - if (version.type === "local") { - versionPath = version.identifier; - // First, create the manifest via droplet. - // This takes up 90% of our progress, so we wrap it in a *0.9 - - manifest = await library.generateDropletManifest( - game.libraryPath, - versionPath, - (value) => { - progress(value * 0.9); - }, - (value) => { - logger.info(value); - }, - ); - fileList = await library.versionReaddir( - game.libraryPath, - versionPath, - ); - logger.info("Created manifest successfully!"); - } else if (version.type === "depot" && unimportedVersion) { - manifest = castManifest(unimportedVersion.manifest); - fileList = unimportedVersion.fileList; - progress(90); - } else { - throw "Could not find or create manifest for this version."; - } - - const currentIndex = await prisma.gameVersion.count({ - where: { gameId: gameId }, - }); - - // Then, create the database object - const newVersion = await prisma.gameVersion.create({ - data: { - game: { - connect: { - id: gameId, + return await taskHandler.create( + { + key: taskKey, + taskGroup: "import:version", + name: `Importing version ${version.name} for ${game.mName}`, + acls: ["system:import:version:read"], + async run({ progress, logger }) { + let versionPath: string | null = null; + let manifest; + let fileList; + + if (version.type === "local") { + versionPath = version.identifier; + // First, create the manifest via droplet. + // This takes up 90% of our progress, so we wrap it in a *0.9 + + manifest = await library.generateDropletManifest( + game.libraryPath, + versionPath, + (value) => { + progress(value * 0.9); }, - }, - - displayName: metadata.displayName ?? null, - - versionPath, - dropletManifest: manifest, - fileList, - versionIndex: currentIndex, - delta: metadata.delta, - - onlySetup: metadata.onlySetup, - setups: { - createMany: { - data: metadata.setups.map((v) => ({ - command: v.launch, - platform: v.platform, - })), + (value) => { + logger.info(value); }, - }, + ); + fileList = await library.versionReaddir( + game.libraryPath, + versionPath, + ); + logger.info("Created manifest successfully!"); + } else if (version.type === "depot" && unimportedVersion) { + manifest = castManifest(unimportedVersion.manifest); + fileList = unimportedVersion.fileList; + progress(90); + } else { + throw "Could not find or create manifest for this version."; + } + + const currentIndex = await prisma.gameVersion.count({ + where: { gameId: gameId }, + }); - launches: { - createMany: !metadata.onlySetup - ? { - data: metadata.launches.map((v) => ({ - name: v.name, - command: v.launch, - platform: v.platform, - ...(v.emulatorId && game.type === "Game" - ? { - emulatorId: v.emulatorId, - } - : undefined), - emulatorSuggestions: - game.type === "Emulator" ? (v.suggestions ?? []) : [], - })), - } - : { data: [] }, - }, - }, - }); - logger.info("Successfully created version!"); - - notificationSystem.systemPush({ - nonce: `version-create-${gameId}-${version}`, - title: `'${game.mName}' ('${version.name}') finished importing.`, - description: `Drop finished importing version ${version.name} for ${game.mName}.`, - actions: [`View|/admin/library/${gameId}`], - acls: ["system:import:version:read"], - }); + // Then, create the database object + const newVersion = await prisma.gameVersion.create({ + data: { + game: { + connect: { + id: gameId, + }, + }, - // Ensure cache is filled (also pre-caches the manifest) - try { - await gameSizeManager.getVersionSize(newVersion.versionId); - } catch (e) { - logger.warn(`Failed to pre-cache game size and manifest: ${e}`); - } + displayName: metadata.displayName ?? null, + + versionPath, + dropletManifest: manifest, + fileList, + versionIndex: currentIndex, + delta: metadata.delta, + + onlySetup: metadata.onlySetup, + setups: { + createMany: { + data: metadata.setups.map((v) => ({ + command: v.launch, + platform: v.platform, + })), + }, + }, - if (version.type === "depot") { - // SAFETY: we can only reach this if the type is depot and identifier is valid - // eslint-disable-next-line drop/no-prisma-delete - await prisma.unimportedGameVersion.delete({ - where: { - id: version.identifier, + launches: { + createMany: !metadata.onlySetup + ? { + data: metadata.launches.map((v) => ({ + name: v.name, + command: v.launch, + platform: v.platform, + ...(v.emulatorId && game.type === "Game" + ? { + emulatorId: v.emulatorId, + } + : undefined), + emulatorSuggestions: + game.type === "Emulator" ? (v.suggestions ?? []) : [], + })), + } + : { data: [] }, + }, }, }); - } - progress(100); + logger.info("Successfully created version!"); + + notificationSystem.systemPush({ + nonce: `version-create-${gameId}-${version}`, + title: `'${game.mName}' ('${version.name}') finished importing.`, + description: `Drop finished importing version ${version.name} for ${game.mName}.`, + actions: [`View|/admin/library/${gameId}`], + acls: ["system:import:version:read"], + }); + + // Ensure cache is filled (also pre-caches the manifest) + try { + await gameSizeManager.getVersionSize(newVersion.versionId); + } catch (e) { + logger.warn(`Failed to pre-cache game size and manifest: ${e}`); + } + + if (version.type === "depot") { + // SAFETY: we can only reach this if the type is depot and identifier is valid + // eslint-disable-next-line drop/no-prisma-delete + await prisma.unimportedGameVersion.delete({ + where: { + id: version.identifier, + }, + }); + } + progress(100); + }, }, - }, parentTask); + parentTask, + ); } async peekFile( From 7ef71659a943563e77d7584940f3a754f6689746 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 18 Feb 2026 16:30:39 +1100 Subject: [PATCH 5/5] feat: library filter util --- .vscode/settings.json | 2 +- components/GameEditor/Version.vue | 2 +- i18n/locales/en_us.json | 22 ++ pages/admin/library/[id]/index.vue | 2 +- pages/admin/library/index.vue | 290 +++++++++++++++--- pages/admin/library/mass-import.vue | 2 +- server/api/v1/admin/game/index.get.ts | 16 - server/api/v1/admin/library/index.get.ts | 12 +- .../api/v1/client/game/[id]/versions.get.ts | 11 +- server/api/v1/client/game/manifest.get.ts | 23 +- server/internal/gamesize/index.ts | 18 +- server/internal/library/index.ts | 9 +- server/internal/library/manifest/index.ts | 14 +- 13 files changed, 333 insertions(+), 90 deletions(-) delete mode 100644 server/api/v1/admin/game/index.get.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a935af9..510ff207 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,7 +29,7 @@ "username": "drop" } ], - "typescript.experimental.useTsgo": true, + "typescript.experimental.useTsgo": false, // prioritize ArkType's "type" for autoimports "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"] } diff --git a/components/GameEditor/Version.vue b/components/GameEditor/Version.vue index 28165fad..59403aa8 100644 --- a/components/GameEditor/Version.vue +++ b/components/GameEditor/Version.vue @@ -89,7 +89,7 @@
- {{ version.versionPath }} + {{ version.versionPath }} {{ version.delta }} {{ version.versionIndex }}
    diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index 50c4722a..a28ed36a 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -368,8 +368,30 @@ "addGames": "All Games", "addToLib": "Add to Library", "admin": { + "nav": { + "nextPagination": "Next", + "backPagination": "Previous", + "sortLabel": "Sort", + "filterLabel": "Filters", + "filterCount": "{0} filters", + "clearAllFilters": "Clear all", + "filters": { + "version": { + "title": "Versions", + "none": "No versions imported", + "available": "Available to import" + }, + "metadata": { + "title": "Metadata", + "noCarousel": "No images in carousel", + "emptyDescription": "Empty description", + "featured": "Featured" + } + } + }, "detectedGame": "Drop has detected you have new games to import.", "detectedVersion": "Drop has detected you have new versions of this game to import.", + "massImportTool": "Mass Import Tool", "fileExtSelector": { "add": "Add \"{0}\"", "noSelected": "No extensions selected." diff --git a/pages/admin/library/[id]/index.vue b/pages/admin/library/[id]/index.vue index 3b9e9308..dc0a1f65 100644 --- a/pages/admin/library/[id]/index.vue +++ b/pages/admin/library/[id]/index.vue @@ -93,7 +93,7 @@ {{ $t("library.admin.openStore") }}
-

Mass Import Tool

+

+ {{ $t("library.admin.massImportTool") }} +

-
- -
+ + +

+ {{ $t("library.admin.nav.filterLabel") }} +

+
+
+
+ + +
+
+ +
+
+
+ +
+
+ + {{ filter.title }} + +
+
+
+
+ + + + + +
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+ + {{ $t("library.admin.nav.sortLabel") }} + +
+ + + + + + +
+
+
+