game version re-ordering

This commit is contained in:
DecDuck
2024-10-14 20:34:23 +11:00
parent 8674ac7211
commit 329c74d3ce
18 changed files with 354 additions and 50 deletions

View File

@ -10,7 +10,7 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@drop/droplet": "^0.4.4", "@drop/droplet": "^0.5.0",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@prisma/client": "5.20.0", "@prisma/client": "5.20.0",
@ -28,7 +28,8 @@
"turndown": "^7.2.0", "turndown": "^7.2.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest",
"vuedraggable": "^4.1.0"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"devDependencies": { "devDependencies": {
@ -46,7 +47,7 @@
"tailwindcss": "^3.4.13" "tailwindcss": "^3.4.13"
}, },
"optionalDependencies": { "optionalDependencies": {
"@drop/droplet-linux-x64-gnu": "^0.4.4", "@drop/droplet-linux-x64-gnu": "^0.5.0",
"@drop/droplet-win32-x64-msvc": "^0.4.4" "@drop/droplet-win32-x64-msvc": "^0.5.0"
} }
} }

View File

@ -74,7 +74,7 @@
</div> </div>
</Listbox> </Listbox>
<div class="flex flex-col gap-4 max-w-md" v-if="versionSettings"> <div class="flex flex-col gap-8 max-w-md" v-if="versionSettings">
<div> <div>
<label <label
for="startup" for="startup"
@ -130,7 +130,41 @@
<PlatformSelector v-model="versionSettings.platform"> <PlatformSelector v-model="versionSettings.platform">
Version platform Version platform
</PlatformSelector> </PlatformSelector>
<LoadingButton @click="startImport_wrapper" class="w-fit" :loading="importLoading"> <SwitchGroup as="div" class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<SwitchLabel
as="span"
class="text-sm font-medium leading-6 text-zinc-100"
passive
>Update mode</SwitchLabel
>
<SwitchDescription as="span" class="text-sm text-zinc-400"
>When enabled, these files will be installed on top of (overwriting)
the previous version's. If multiple "update modes" are chained
together, they are applied in order.</SwitchDescription
>
</span>
<Switch
v-model="delta"
:class="[
delta ? 'bg-blue-600' : 'bg-zinc-800',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
]"
>
<span
aria-hidden="true"
:class="[
delta ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
]"
/>
</Switch>
</SwitchGroup>
<LoadingButton
@click="startImport_wrapper"
class="w-fit"
:loading="importLoading"
>
Import Import
</LoadingButton> </LoadingButton>
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4"> <div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
@ -180,6 +214,10 @@ import {
ListboxLabel, ListboxLabel,
ListboxOption, ListboxOption,
ListboxOptions, ListboxOptions,
Switch,
SwitchDescription,
SwitchGroup,
SwitchLabel,
} from "@headlessui/vue"; } from "@headlessui/vue";
import { XCircleIcon } from "@heroicons/vue/16/solid"; import { XCircleIcon } from "@heroicons/vue/16/solid";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
@ -203,6 +241,7 @@ const currentlySelectedVersion = ref(-1);
const versionSettings = ref< const versionSettings = ref<
{ platform: string; startup: string; setup: string } | undefined { platform: string; startup: string; setup: string } | undefined
>(); >();
const delta = ref(false);
const importLoading = ref(false); const importLoading = ref(false);
const importError = ref<string | undefined>(); const importError = ref<string | undefined>();
@ -233,6 +272,7 @@ async function startImport() {
platform: versionSettings.value.platform, platform: versionSettings.value.platform,
startup: versionSettings.value.startup, startup: versionSettings.value.startup,
setup: versionSettings.value.setup, setup: versionSettings.value.setup,
delta: delta.value
}, },
}); });
router.push(`/admin/task/${taskId.taskId}`); router.push(`/admin/task/${taskId.taskId}`);

View File

@ -11,20 +11,18 @@
class="mt-5 pt-5 border-t border-zinc-700 prose prose-invert prose-blue" class="mt-5 pt-5 border-t border-zinc-700 prose prose-invert prose-blue"
></div> ></div>
</div> </div>
<div> <div class="space-y-8">
<div class="px-4 py-3 bg-gray-950 rounded"> <div class="px-4 py-3 bg-gray-950 rounded">
<div class="border-b border-zinc-800 pb-3"> <div class="border-b border-zinc-800 pb-3">
<div <div
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap" class="flex flex-wrap items-center justify-between sm:flex-nowrap"
> >
<div class="ml-4 mt-2">
<h3 <h3
class="text-base font-semibold font-display leading-6 text-zinc-100" class="text-base font-semibold font-display leading-6 text-zinc-100"
> >
Images Images
</h3> </h3>
</div> <div class="flex-shrink-0">
<div class="ml-4 mt-2 flex-shrink-0">
<button <button
@click="() => (showUploadModal = true)" @click="() => (showUploadModal = true)"
type="button" type="button"
@ -90,6 +88,39 @@
</div> </div>
</div> </div>
</div> </div>
<div class="py-5 px-6 bg-gray-950 rounded">
<h1 class="text-2xl font-semibold font-display text-zinc-100">
Manage version order
</h1>
<div class="text-center w-full text-sm text-zinc-600">lowest</div>
<draggable
@update="() => updateVersionOrder()"
:list="game.versions"
handle=".handle"
class="mt-2 space-y-4"
>
<template #item="{ element: item }: { element: GameVersion }">
<div
class="w-full inline-flex items-center px-4 py-2 bg-zinc-900 rounded justify-between"
>
<div class="text-zinc-100 font-semibold">
{{ item.versionName }}
</div>
<div class="text-zinc-400">
{{ item.delta ? "Upgrade mode" : "" }}
</div>
<div class="inline-flex gap-x-2">
<Bars3Icon class="cursor-move w-6 h-6 text-zinc-400 handle" />
<button @click="() => deleteVersion(item.versionName)">
<TrashIcon class="w-5 h-5 text-red-600" />
</button>
</div>
</div>
</template>
</draggable>
<div class="mt-2 text-center w-full text-sm text-zinc-600">highest</div>
<span class="text-zinc-100">{{ game.versions }}</span>
</div>
</div> </div>
</div> </div>
<UploadFileDialog <UploadFileDialog
@ -102,7 +133,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Game } from "@prisma/client"; import { Bars3Icon, TrashIcon } from "@heroicons/vue/16/solid";
import type { Game, GameVersion } from "@prisma/client";
import markdownit from "markdown-it"; import markdownit from "markdown-it";
import UploadFileDialog from "~/components/UploadFileDialog.vue"; import UploadFileDialog from "~/components/UploadFileDialog.vue";
@ -116,9 +148,12 @@ const route = useRoute();
const gameId = route.params.id.toString(); const gameId = route.params.id.toString();
const headers = useRequestHeaders(["cookie"]); const headers = useRequestHeaders(["cookie"]);
const game = ref( const game = ref(
await $fetch<Game>(`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`, { await $fetch(
`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`,
{
headers, headers,
}) }
)
); );
const md = markdownit(); const md = markdownit();
@ -167,4 +202,29 @@ async function uploadAfterImageUpload(result: Game) {
if (!game.value) return; if (!game.value) return;
game.value.mImageLibrary = result.mImageLibrary; game.value.mImageLibrary = result.mImageLibrary;
} }
async function deleteVersion(versionName: string) {
await $fetch("/api/v1/admin/game/version", {
method: "DELETE",
body: {
id: gameId,
versionName: versionName,
},
});
game.value.versions.splice(
game.value.versions.findIndex((e) => e.versionName === versionName),
1
);
}
async function updateVersionOrder() {
const newVersions = await $fetch("/api/v1/admin/game/version", {
method: "POST",
body: {
id: gameId,
versions: game.value.versions.map((e) => e.versionName),
},
});
game.value.versions = newVersions;
}
</script> </script>

5
plugins/vuedraggable.ts Normal file
View File

@ -0,0 +1,5 @@
import draggable from "vuedraggable";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component("draggable", draggable);
});

View File

@ -0,0 +1,9 @@
/*
Warnings:
- Added the required column `versionIndex` to the `GameVersion` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "GameVersion" ADD COLUMN "delta" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "versionIndex" INTEGER NOT NULL;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `versionOrder` on the `Game` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Game" DROP COLUMN "versionOrder";

View File

@ -91,7 +91,6 @@ model Game {
mCoverId String mCoverId String
mImageLibrary String[] // linked to objects in s3 mImageLibrary String[] // linked to objects in s3
versionOrder String[]
versions GameVersion[] versions GameVersion[]
libraryBasePath String @unique // Base dir for all the game versions libraryBasePath String @unique // Base dir for all the game versions
@ -109,6 +108,9 @@ model GameVersion {
setupCommand String // Command to setup game (dependencies and such) setupCommand String // Command to setup game (dependencies and such)
dropletManifest Json // Results from droplet dropletManifest Json // Results from droplet
versionIndex Int
delta Boolean @default(false)
@@id([gameId, versionName]) @@id([gameId, versionName])
} }

View File

@ -16,6 +16,19 @@ export default defineEventHandler(async (h3) => {
where: { where: {
id: gameId, id: gameId,
}, },
include: {
versions: {
orderBy: {
versionIndex: "asc",
},
select: {
versionIndex: true,
versionName: true,
platform: true,
delta: true,
}
},
},
}); });
if (!game) if (!game)

View File

@ -0,0 +1,26 @@
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3);
if (!user) throw createError({ statusCode: 403 });
const body = await readBody(h3);
const gameId = body.id.toString();
const version = body.versionName.toString();
if (!gameId || !version)
throw createError({
statusCode: 400,
statusMessage: "Missing ID or versionName in body",
});
await prisma.gameVersion.delete({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
},
});
return {};
});

View File

@ -0,0 +1,40 @@
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3);
if (!user) throw createError({ statusCode: 403 });
const body = await readBody(h3);
const gameId = body.id?.toString();
// We expect an array of the version names for this game
const versions: string[] | undefined = body.versions;
if (!gameId || !versions || !Array.isArray(versions))
throw createError({
statusCode: 400,
statusMessage: "Missing id, versions or versions is not an array",
});
const newVersions = await prisma.$transaction(
versions.map((versionName, versionIndex) =>
prisma.gameVersion.update({
where: {
gameId_versionName: {
gameId: gameId,
versionName: versionName,
},
},
data: {
versionIndex: versionIndex,
},
select: {
versionIndex: true,
versionName: true,
platform: true,
delta: true,
}
})
)
);
return newVersions;
});

View File

@ -10,23 +10,24 @@ export default defineEventHandler(async (h3) => {
const platform = body.platform; const platform = body.platform;
const startup = body.startup; const startup = body.startup;
const setup = body.setup ?? ""; const setup = body.setup ?? "";
if ( const delta = body.delta ?? false;
!gameId || if (!gameId || !versionName || !platform || (!delta && !startup))
!versionName ||
!platform ||
!startup
)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: statusMessage:
"Missing id, version, platform, setup or startup from body", "Missing id, version, platform, setup or startup from body",
}); });
const taskId = await libraryManager.importVersion(gameId, versionName, { const taskId = await libraryManager.importVersion(
gameId,
versionName,
{
platform, platform,
startup, startup,
setup, setup,
}); },
delta
);
if (!taskId) if (!taskId)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,

View File

@ -0,0 +1,12 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3);
const id = query.id?.toString();
const version = query.version?.toString();
if (!id || !version)
throw createError({
statusCode: 400,
statusMessage: "Missing id or version in query",
});
});

View File

@ -14,6 +14,14 @@ export default defineClientEventHandler(async (h3, {}) => {
where: { where: {
gameId: id, gameId: id,
}, },
select: {
versionIndex: true,
versionName: true,
platform: true,
setupCommand: true,
launchCommand: true,
delta: true,
}
}); });
return versions; return versions;

View File

@ -1,8 +1,16 @@
# Drop Download System # Drop P2P System
Drop downloads come in two types:
Drop clients have a variety of P2P or P2P-like methods of data transfer available
## Public (not quite) HTTPS downloads endpoints ## Public (not quite) HTTPS downloads endpoints
These use public HTTPS certificate, and while are authenticated, are 'public' in the sense that they aren't P2P; anyone can connect to them These use public HTTPS certificate, and while are authenticated, are 'public' in the sense that they aren't P2P; anyone can connect to them
## Private mTLS P2P endpoints ## Private mTLS P2P endpoints
Drop clients use P2P mTLS aided by the P2P co-ordinator to transfer chunks between themselves.
Drop clients use P2P mTLS aided by the P2P co-ordinator to transfer chunks between themselves. This happens over HTTP.
## Private mTLS Wireguard tunnels
Drop clients can establish P2P Wireguard

View File

@ -0,0 +1,52 @@
export type DropChunk = {
permissions: number;
ids: string[];
checksums: string[];
lengths: string[];
};
export type DropManifest = {
[key: string]: DropChunk;
};
export type DropManifestMetadata = {
manifest: DropManifest;
versionName: string;
};
export type DropGeneratedManifest = DropManifest & {
[key: string]: { versionName: string };
};
class ManifestGenerator {
static generateManifest(
rootManifest: DropManifestMetadata,
...overlays: DropManifestMetadata[]
): DropGeneratedManifest {
if (overlays.length == 0) {
return Object.fromEntries(
Object.entries(rootManifest.manifest).map(([key, value]) => [
key,
Object.assign({}, value, { versionName: rootManifest.versionName }),
])
);
}
// Recurse in verse order through versions, skipping files that already exist.
const versions = [...overlays.reverse(), rootManifest];
const manifest: DropGeneratedManifest = {};
for (const version of versions) {
for (const [filename, chunk] of Object.entries(version.manifest)) {
if (manifest[filename]) continue;
manifest[filename] = Object.assign({}, chunk, {
versionName: version.versionName,
});
}
}
return manifest;
}
}
export const manifestGenerator = new ManifestGenerator();
export default manifestGenerator;

View File

@ -195,7 +195,8 @@ class LibraryManager {
async importVersion( async importVersion(
gameId: string, gameId: string,
versionName: string, versionName: string,
metadata: { platform: string; setup: string; startup: string } metadata: { platform: string; setup: string; startup: string },
delta = false
) { ) {
const taskId = `import:${gameId}:${versionName}`; const taskId = `import:${gameId}:${versionName}`;
@ -238,6 +239,10 @@ class LibraryManager {
log("Created manifest successfully!"); log("Created manifest successfully!");
const currentIndex = await prisma.gameVersion.count({
where: { gameId: gameId },
});
// Then, create the database object // Then, create the database object
const version = await prisma.gameVersion.create({ const version = await prisma.gameVersion.create({
data: { data: {
@ -247,6 +252,8 @@ class LibraryManager {
setupCommand: metadata.setup, setupCommand: metadata.setup,
launchCommand: metadata.startup, launchCommand: metadata.startup,
dropletManifest: manifest, dropletManifest: manifest,
versionIndex: currentIndex,
delta: delta,
}, },
}); });

View File

@ -296,23 +296,23 @@
dependencies: dependencies:
mime "^3.0.0" mime "^3.0.0"
"@drop/droplet-linux-x64-gnu@0.4.4", "@drop/droplet-linux-x64-gnu@^0.4.4": "@drop/droplet-linux-x64-gnu@0.5.0", "@drop/droplet-linux-x64-gnu@^0.5.0":
version "0.4.4" version "0.5.0"
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.4.4.tgz#6678a0923bb13d37e20cae467f45c72bc5d9fe6e" resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.5.0.tgz#06643f7bc79de4b35a395295ef1e29fad46f32b5"
integrity sha1-ZnigkjuxPTfiDK5Gf0XHK8XZ/m4= integrity sha1-BmQ/e8ed5LNaOVKV7x4p+tRvMrU=
"@drop/droplet-win32-x64-msvc@0.4.4", "@drop/droplet-win32-x64-msvc@^0.4.4": "@drop/droplet-win32-x64-msvc@0.5.0", "@drop/droplet-win32-x64-msvc@^0.5.0":
version "0.4.4" version "0.5.0"
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.4.4.tgz#10802bb36c6ec7d69aa17ea22081e5d5f0dac3c3" resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.5.0.tgz#abc02af2102f0faaf4561473b7a18395a0ba2b10"
integrity sha1-EIArs2xux9aaoX6iIIHl1fDaw8M= integrity sha1-q8Aq8hAvD6r0VhRzt6GDlaC6KxA=
"@drop/droplet@^0.4.4": "@drop/droplet@^0.5.0":
version "0.4.4" version "0.5.0"
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.4.4.tgz#a9b6e3a341e85703b25c7fee597261e1b239a280" resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.5.0.tgz#27da4f7292c9b860d38bb785c0fb2bb3b9cd50a3"
integrity sha1-qbbjo0HoVwOyXH/uWXJh4bI5ooA= integrity sha1-J9pPcpLJuGDTi7eFwPsrs7nNUKM=
optionalDependencies: optionalDependencies:
"@drop/droplet-linux-x64-gnu" "0.4.4" "@drop/droplet-linux-x64-gnu" "0.5.0"
"@drop/droplet-win32-x64-msvc" "0.4.4" "@drop/droplet-win32-x64-msvc" "0.5.0"
"@esbuild/aix-ppc64@0.20.2": "@esbuild/aix-ppc64@0.20.2":
version "0.20.2" version "0.20.2"
@ -4887,6 +4887,11 @@ smob@^1.0.0:
resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab"
integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==
sortablejs@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@ -5656,6 +5661,13 @@ vue@^3.5.5, vue@latest:
"@vue/server-renderer" "3.5.10" "@vue/server-renderer" "3.5.10"
"@vue/shared" "3.5.10" "@vue/shared" "3.5.10"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"