diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0570fe39..510ff207 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,11 +4,7 @@
"strings": "on"
},
"i18n-ally.extract.autoDetect": true,
- "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.ignored": ["string >= 14", "string.alphanumeric >= 5"],
"i18n-ally.extract.ignoredByFiles": {
"components/NewsArticleCreateButton.vue": ["[", "`", "Enter"],
"pages/admin/library/sources/index.vue": ["Filesystem"],
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/components/LogLine.vue b/components/LogLine.vue
index 64fbf2fc..8c5d7558 100644
--- a/components/LogLine.vue
+++ b/components/LogLine.vue
@@ -1,6 +1,6 @@
{{ log.timestamp }}
+ >{{ log.time }}
{{ log.level }}
- {{
- log.message
- }}
+ {{ log.prefix }}
+ {{ log.msg }}
diff --git a/components/ProgressBar.vue b/components/ProgressBar.vue
index 2ca6f299..a28ff003 100644
--- a/components/ProgressBar.vue
+++ b/components/ProgressBar.vue
@@ -13,7 +13,7 @@
class="absolute inset-0 flex items-center justify-center text-blue-200 text-sm font-bold font-display"
>
- {{ $n(Math.round(percentage) / 100, "percent") }}
+ {{ $n(Math.round(percentage * 100) / 10000, "percent") }}
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/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/[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") }}
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ $t("library.admin.massImportTool") }}
+
+
+
+
+
+ {{ $t("chars.arrow") }}
+
+
+
+
+
-
-
- {{ $t("library.admin.detectedGame") }}
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ $t("library.admin.detectedGame") }}
+
+
+
-
- {{ $t("chars.arrow") }}
-
-
-
-
+
+
+ {{ $t("chars.arrow") }}
+
+
+
+
+
-
-
-
-
+
+
+
+ {{ $t("library.admin.nav.filterLabel") }}
+
+
+
+
+
+
+ {{
+ $t("library.admin.nav.filterCount", [
+ Object.values(currentFilters).filter((v) => v).length,
+ ])
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t("common.noResults") }}
- {{ $t("library.admin.noGames") }}
-
-
{{
@@ -280,7 +445,77 @@
+
+
+
+
+ {{ $t("common.srLoading") }}
+
+
+
@@ -293,8 +528,23 @@ import {
ArrowTopRightOnSquareIcon,
InformationCircleIcon,
StarIcon,
+ WrenchScrewdriverIcon,
+ ArrowLongLeftIcon,
+ ArrowLongRightIcon,
+ ChevronDownIcon,
+ FunnelIcon,
} from "@heroicons/vue/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
+import type { AdminLibraryGame } from "~/server/api/v1/admin/library/index.get";
+import {
+ Disclosure,
+ DisclosureButton,
+ DisclosurePanel,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuItems,
+} from "@headlessui/vue";
const { t } = useI18n();
@@ -308,31 +558,72 @@ useHead({
const searchQuery = ref("");
-const libraryState = await $dropFetch("/api/v1/admin/library");
-type LibraryStateGame = (typeof libraryState.games)[number]["game"];
+const { unimportedGames, hasLibraries } = await $dropFetch(
+ "/api/v1/admin/library/libraries",
+);
+
+const route = useRoute();
+const router = useRouter();
-const toImport = ref(
- Object.values(libraryState.unimportedGames).flat().length > 0,
+// Hard limit on server
+const pageSize = 24;
+const currentIndex = ref(
+ route.query.page ? parseInt(route.query.page.toString()) - 1 : 0,
);
+const maxIndex = ref(0);
+const maxPages = computed(() => Math.ceil(maxIndex.value / pageSize));
-const libraryGames = ref<
- Array<
- LibraryStateGame & {
- status: "online" | "offline";
- hasNotifications?: boolean;
- notifications: {
- noVersions?: boolean;
- toImport?: boolean;
- offline?: boolean;
- };
- }
- >
->(
- libraryState.games.map((e) => {
+const games = ref ([]);
+const gamesLoading = ref(false);
+
+async function fetchPage() {
+ gamesLoading.value = true;
+ const { results, count } = await $dropFetch("/api/v1/admin/library", {
+ query: {
+ skip: currentIndex.value * pageSize,
+ limit: 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 = computed(() =>
+ games.value.map((e) => {
if (e.status == "offline") {
return {
...e.game,
- status: "offline" as const,
+ status: "offline",
hasNotifications: true,
notifications: {
offline: true,
@@ -355,19 +646,6 @@ const libraryGames = ref<
}),
);
-const filteredLibraryGames = computed(() =>
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore excessively deep ts
- libraryGames.value.filter((e) => {
- if (!searchQuery.value) return true;
- const searchQueryLower = searchQuery.value.toLowerCase();
- if (e.mName.toLowerCase().includes(searchQueryLower)) return true;
- if (e.mShortDescription.toLowerCase().includes(searchQueryLower))
- return true;
- return false;
- }),
-);
-
async function deleteGame(id: string) {
await $dropFetch(`/api/v1/admin/game/${id}`, {
method: "DELETE",
@@ -396,4 +674,73 @@ async function featureGame(id: string) {
libraryGames.value[gameIndex].featured = !game.featured;
gameFeatureLoading.value[game.id] = false;
}
+
+const currentFilters = ref<{ [key: string]: boolean }>({});
+
+function createFilterKey(
+ filter: { value: string },
+ subfilter: { value: string },
+) {
+ return `${filter.value}.${subfilter.value}`;
+}
+
+const filters = computed(
+ () =>
+ ({
+ version: [
+ {
+ value: "none",
+ label: t("library.admin.nav.filters.version.none"),
+ },
+ {
+ value: "available",
+ label: t("library.admin.nav.filters.version.available"),
+ },
+ ],
+ metadata: [
+ {
+ value: "featured",
+ label: t("library.admin.nav.filters.metadata.featured"),
+ },
+ {
+ value: "noCarousel",
+ label: t("library.admin.nav.filters.metadata.noCarousel"),
+ },
+ {
+ value: "emptyDescription",
+ label: t("library.admin.nav.filters.metadata.emptyDescription"),
+ },
+ ],
+ }) as const,
+);
+
+const filterScaffold = computed(
+ () =>
+ ({
+ version: {
+ title: t("library.admin.nav.filters.version.title"),
+ value: "version",
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ values: filters.value.version as any,
+ },
+ metadata: {
+ title: t("library.admin.nav.filters.metadata.title"),
+ value: "metadata",
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ values: filters.value.metadata as any,
+ },
+ }) satisfies {
+ [key in keyof typeof filters.value]: {
+ title: string;
+ value: string;
+ values: Array<{ value: string; label: string }>;
+ };
+ },
+);
+
+const sortOptions = [
+ { name: "Most Popular", href: "#", current: true },
+ { name: "Best Rating", href: "#", current: false },
+ { name: "Newest", href: "#", current: false },
+];
diff --git a/pages/admin/library/mass-import.vue b/pages/admin/library/mass-import.vue
new file mode 100644
index 00000000..6b1f87dc
--- /dev/null
+++ b/pages/admin/library/mass-import.vue
@@ -0,0 +1,351 @@
+
+
+
+
+
+ Mass Import Tool
+
+
+ Quickly import a large amount of versions at once.
+
+
+
+
+ Import →
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+ Name
+ |
+
+ Type
+ |
+
+ Display Name
+ |
+
+ Setup Mode
+ |
+
+
+
+
+
+
+
+ ![]()
+ {{ game.name }}
+
+ |
+
+
+ |
+
+
+
+ |
+
+ {{ version.name }}
+ |
+
+ {{ version.type }}
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/admin/task/[id]/index.vue b/pages/admin/task/[id]/index.vue
index bcdb0a4f..79d52346 100644
--- a/pages/admin/task/[id]/index.vue
+++ b/pages/admin/task/[id]/index.vue
@@ -40,7 +40,7 @@
-
+
-
-
-
+
+
{{ $t("tasks.admin.completedTasksTitle") }}
-
@@ -120,6 +171,7 @@
|