feat: minimum support for unrefed object cleanup

This commit is contained in:
Huskydog9988
2025-05-08 19:20:34 -04:00
committed by DecDuck
parent 892e46801a
commit 8a9b2bfc87
16 changed files with 100 additions and 76 deletions
+2 -2
View File
@@ -9,7 +9,7 @@
class="absolute inset-0 transition-all duration-300 group-hover:scale-110" class="absolute inset-0 transition-all duration-300 group-hover:scale-110"
> >
<img <img
:src="useObject(game.mCoverId)" :src="useObject(game.mCoverObjectId)"
class="w-full h-full object-cover brightness-[90%]" class="w-full h-full object-cover brightness-[90%]"
:class="{ active: active === game.id }" :class="{ active: active === game.id }"
:alt="game.mName" :alt="game.mName"
@@ -42,7 +42,7 @@ const props = defineProps<{
game: game:
| SerializeObject<{ | SerializeObject<{
id: string; id: string;
mCoverId: string; mCoverObjectId: string;
mName: string; mName: string;
mShortDescription: string; mShortDescription: string;
}> }>
+1 -1
View File
@@ -34,7 +34,7 @@
class="flex flex-row items-center w-full p-1 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95" class="flex flex-row items-center w-full p-1 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95"
> >
<img <img
:src="useObject(game.mCoverId)" :src="useObject(game.mCoverObjectId)"
class="h-9 w-9 flex-shrink-0 rounded transition-all duration-300 group-hover:scale-105 hover:rotate-[-2deg] hover:shadow-lg" class="h-9 w-9 flex-shrink-0 rounded transition-all duration-300 group-hover:scale-105 hover:rotate-[-2deg] hover:shadow-lg"
alt="" alt=""
/> />
+33 -28
View File
@@ -10,7 +10,7 @@
class="flex flex-col lg:flex-row lg:justify-between items-start lg:items-center gap-2" class="flex flex-col lg:flex-row lg:justify-between items-start lg:items-center gap-2"
> >
<div class="inline-flex items-center gap-4"> <div class="inline-flex items-center gap-4">
<img :src="useObject(game.mIconId)" class="size-20" /> <img :src="useObject(game.mIconObjectId)" class="size-20" />
<div> <div>
<h1 class="text-5xl font-bold font-display text-zinc-100"> <h1 class="text-5xl font-bold font-display text-zinc-100">
{{ game.mName }} {{ game.mName }}
@@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<div <div
v-if="game.mImageCarousel.length == 0" v-if="game.mImageCarouselObjectIds.length == 0"
class="text-zinc-400 text-center py-8" class="text-zinc-400 text-center py-8"
> >
No images added to the carousel yet. No images added to the carousel yet.
@@ -64,7 +64,7 @@
<draggable <draggable
v-else v-else
:list="game.mImageCarousel" :list="game.mImageCarouselObjectIds"
class="w-full flex flex-row gap-x-4 overflow-x-auto my-2 py-4" class="w-full flex flex-row gap-x-4 overflow-x-auto my-2 py-4"
@update="() => updateImageCarousel()" @update="() => updateImageCarousel()"
> >
@@ -242,7 +242,7 @@
</div> </div>
<div class="mt-3 grid grid-cols-2 grid-flow-dense gap-8"> <div class="mt-3 grid grid-cols-2 grid-flow-dense gap-8">
<div <div
v-for="(image, imageIdx) in game.mImageLibrary" v-for="(image, imageIdx) in game.mImageLibraryObjectIds"
:key="imageIdx" :key="imageIdx"
class="group relative flex items-center bg-zinc-950/30" class="group relative flex items-center bg-zinc-950/30"
> >
@@ -251,7 +251,7 @@
class="transition-all lg:opacity-0 lg:group-hover:opacity-100 absolute inset-0 flex flex-col items-center justify-center gap-y-2 bg-zinc-950/50" class="transition-all lg:opacity-0 lg:group-hover:opacity-100 absolute inset-0 flex flex-col items-center justify-center gap-y-2 bg-zinc-950/50"
> >
<button <button
v-if="image !== game.mBannerId" v-if="image !== game.mBannerObjectId"
type="button" type="button"
class="inline-flex items-center gap-x-1.5 rounded-md bg-blue-600 px-1.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" class="inline-flex items-center gap-x-1.5 rounded-md bg-blue-600 px-1.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="() => updateBannerImage(image)" @click="() => updateBannerImage(image)"
@@ -259,7 +259,7 @@
Set as banner Set as banner
</button> </button>
<button <button
v-if="image !== game.mCoverId" v-if="image !== game.mCoverObjectId"
type="button" type="button"
class="inline-flex items-center gap-x-1.5 rounded-md bg-blue-600 px-1.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" class="inline-flex items-center gap-x-1.5 rounded-md bg-blue-600 px-1.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="() => updateCoverImage(image)" @click="() => updateCoverImage(image)"
@@ -275,14 +275,17 @@
</button> </button>
</div> </div>
<div <div
v-if="image === game.mBannerId || image === game.mCoverId" v-if="
image === game.mBannerObjectId ||
image === game.mCoverObjectId
"
class="absolute bottom-0 left-0 bg-zinc-950/75 text-zinc-100 text-sm font-semibold px-2 py-1 rounded-tr" class="absolute bottom-0 left-0 bg-zinc-950/75 text-zinc-100 text-sm font-semibold px-2 py-1 rounded-tr"
> >
current current
{{ {{
[ [
image === game.mBannerId ? "banner" : undefined, image === game.mBannerObjectId ? "banner" : undefined,
image === game.mCoverId ? "cover" : undefined, image === game.mCoverObjectId ? "cover" : undefined,
] ]
.filter((e) => e) .filter((e) => e)
.join(" & ") .join(" & ")
@@ -400,7 +403,7 @@
<template #default> <template #default>
<div class="grid grid-cols-2 grid-flow-dense gap-4"> <div class="grid grid-cols-2 grid-flow-dense gap-4">
<div <div
v-for="(image, imageIdx) in game.mImageLibrary" v-for="(image, imageIdx) in game.mImageLibraryObjectIds"
:key="imageIdx" :key="imageIdx"
class="group relative flex items-center bg-zinc-950/30" class="group relative flex items-center bg-zinc-950/30"
> >
@@ -418,7 +421,7 @@
</div> </div>
</div> </div>
<div <div
v-if="game.mImageLibrary.length == 0" v-if="game.mImageLibraryObjectIds.length == 0"
class="text-zinc-400 col-span-2" class="text-zinc-400 col-span-2"
> >
No images to add. No images to add.
@@ -548,7 +551,7 @@ const game = ref(rawGame);
const coreMetadataName = ref(game.value.mName); const coreMetadataName = ref(game.value.mName);
const coreMetadataDescription = ref(game.value.mShortDescription); const coreMetadataDescription = ref(game.value.mShortDescription);
const coreMetadataIconUrl = ref(useObject(game.value.mIconId)); const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
const coreMetadataIconFileUpload = ref<FileList | undefined>(); const coreMetadataIconFileUpload = ref<FileList | undefined>();
const coreMetadataLoading = ref(false); const coreMetadataLoading = ref(false);
@@ -665,8 +668,8 @@ watch(descriptionHTML, (_v) => {
}); });
const validAddCarouselImages = computed(() => const validAddCarouselImages = computed(() =>
game.value.mImageLibrary.filter( game.value.mImageLibraryObjectIds.filter(
(e) => !game.value.mImageCarousel.includes(e), (e) => !game.value.mImageCarouselObjectIds.includes(e),
), ),
); );
@@ -683,15 +686,15 @@ function insertImageAtCursor(id: string) {
async function updateBannerImage(id: string) { async function updateBannerImage(id: string) {
try { try {
if (game.value.mBannerId == id) return; if (game.value.mBannerObjectId == id) return;
const { mBannerId } = await $dropFetch("/api/v1/admin/game", { const { mBannerObjectId } = await $dropFetch("/api/v1/admin/game", {
method: "PATCH", method: "PATCH",
body: { body: {
id: gameId, id: gameId,
mBannerId: id, mBannerId: id,
}, },
}); });
game.value.mBannerId = mBannerId; game.value.mBannerObjectId = mBannerObjectId;
} catch (e) { } catch (e) {
createModal( createModal(
ModalType.Notification, ModalType.Notification,
@@ -710,15 +713,15 @@ async function updateBannerImage(id: string) {
async function updateCoverImage(id: string) { async function updateCoverImage(id: string) {
try { try {
if (game.value.mCoverId == id) return; if (game.value.mCoverObjectId == id) return;
const { mCoverId } = await $dropFetch("/api/v1/admin/game", { const { mCoverObjectId } = await $dropFetch("/api/v1/admin/game", {
method: "PATCH", method: "PATCH",
body: { body: {
id: gameId, id: gameId,
mCoverId: id, mCoverId: id,
}, },
}); });
game.value.mCoverId = mCoverId; game.value.mCoverObjectId = mCoverObjectId;
} catch (e) { } catch (e) {
createModal( createModal(
ModalType.Notification, ModalType.Notification,
@@ -737,7 +740,7 @@ async function updateCoverImage(id: string) {
async function deleteImage(id: string) { async function deleteImage(id: string) {
try { try {
const { mBannerId, mImageLibrary } = await $dropFetch( const { mBannerObjectId, mImageLibraryObjectIds } = await $dropFetch(
"/api/v1/admin/game/image", "/api/v1/admin/game/image",
{ {
method: "DELETE", method: "DELETE",
@@ -747,8 +750,8 @@ async function deleteImage(id: string) {
}, },
}, },
); );
game.value.mImageLibrary = mImageLibrary; game.value.mImageLibraryObjectIds = mImageLibraryObjectIds;
game.value.mBannerId = mBannerId; game.value.mBannerObjectId = mBannerObjectId;
} catch (e) { } catch (e) {
createModal( createModal(
ModalType.Notification, ModalType.Notification,
@@ -767,7 +770,7 @@ async function deleteImage(id: string) {
async function uploadAfterImageUpload(result: Game) { async function uploadAfterImageUpload(result: Game) {
if (!game.value) return; if (!game.value) return;
game.value.mImageLibrary = result.mImageLibrary; game.value.mImageLibraryObjectIds = result.mImageLibraryObjectIds;
} }
async function deleteVersion(versionName: string) { async function deleteVersion(versionName: string) {
@@ -827,13 +830,15 @@ async function updateVersionOrder() {
function addImageToCarousel(id: string) { function addImageToCarousel(id: string) {
showAddCarouselModal.value = false; showAddCarouselModal.value = false;
game.value.mImageCarousel.push(id); game.value.mImageCarouselObjectIds.push(id);
updateImageCarousel(); updateImageCarousel();
} }
function removeImageFromCarousel(id: string) { function removeImageFromCarousel(id: string) {
const imageIndex = game.value.mImageCarousel.findIndex((e) => e == id); const imageIndex = game.value.mImageCarouselObjectIds.findIndex(
game.value.mImageCarousel.splice(imageIndex, 1); (e) => e == id,
);
game.value.mImageCarouselObjectIds.splice(imageIndex, 1);
updateImageCarousel(); updateImageCarousel();
} }
@@ -843,7 +848,7 @@ async function updateImageCarousel() {
method: "PATCH", method: "PATCH",
body: { body: {
id: gameId, id: gameId,
mImageCarousel: game.value.mImageCarousel, mImageCarousel: game.value.mImageCarouselObjectIds,
}, },
}); });
} catch (e) { } catch (e) {
+1 -1
View File
@@ -67,7 +67,7 @@
<div class="flex flex-1 flex-row p-4 gap-x-4"> <div class="flex flex-1 flex-row p-4 gap-x-4">
<img <img
class="h-16 w-16 flex-shrink-0 rounded-md" class="h-16 w-16 flex-shrink-0 rounded-md"
:src="useObject(game.mIconId)" :src="useObject(game.mIconObjectId)"
alt="" alt=""
/> />
<div class="flex flex-col"> <div class="flex flex-col">
+6 -3
View File
@@ -6,7 +6,7 @@
<!-- Banner background with gradient overlays --> <!-- Banner background with gradient overlays -->
<div class="absolute inset-0 z-0 rounded-xl overflow-hidden"> <div class="absolute inset-0 z-0 rounded-xl overflow-hidden">
<img <img
:src="useObject(game.mBannerId)" :src="useObject(game.mBannerObjectId)"
class="w-full h-[24rem] object-cover blur-sm scale-105" class="w-full h-[24rem] object-cover blur-sm scale-105"
/> />
<div <div
@@ -73,13 +73,16 @@
</h2> </h2>
<div class="relative"> <div class="relative">
<VueCarousel :items-to-show="1"> <VueCarousel :items-to-show="1">
<VueSlide v-for="image in game.mImageCarousel" :key="image"> <VueSlide
v-for="image in game.mImageCarouselObjectIds"
:key="image"
>
<img <img
class="w-fit h-48 lg:h-96 rounded" class="w-fit h-48 lg:h-96 rounded"
:src="useObject(image)" :src="useObject(image)"
/> />
</VueSlide> </VueSlide>
<VueSlide v-if="game.mImageCarousel.length == 0"> <VueSlide v-if="game.mImageCarouselObjectIds.length == 0">
<div <div
class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display" class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display"
> >
+10 -4
View File
@@ -5,7 +5,10 @@
> >
<!-- banner image --> <!-- banner image -->
<div class="absolute flex top-0 h-fit inset-x-0 h-12 -z-[20] pb-4"> <div class="absolute flex top-0 h-fit inset-x-0 h-12 -z-[20] pb-4">
<img :src="useObject(game.mBannerId)" class="blur-sm w-full h-auto" /> <img
:src="useObject(game.mBannerObjectId)"
class="blur-sm w-full h-auto"
/>
<div <div
class="absolute inset-0 bg-gradient-to-b from-transparent to-80% to-zinc-950" class="absolute inset-0 bg-gradient-to-b from-transparent to-80% to-zinc-950"
/> />
@@ -29,7 +32,7 @@
> >
<img <img
class="transition-all duration-300 hover:scale-105 hover:rotate-[-1deg] w-64 h-auto rounded gameCover" class="transition-all duration-300 hover:scale-105 hover:rotate-[-1deg] w-64 h-auto rounded gameCover"
:src="useObject(game.mCoverId)" :src="useObject(game.mCoverObjectId)"
:alt="game.mName" :alt="game.mName"
/> />
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
@@ -115,13 +118,16 @@
</p> </p>
<div class="mt-6 py-4 rounded"> <div class="mt-6 py-4 rounded">
<VueCarousel :items-to-show="1"> <VueCarousel :items-to-show="1">
<VueSlide v-for="image in game.mImageCarousel" :key="image"> <VueSlide
v-for="image in game.mImageCarouselObjectIds"
:key="image"
>
<img <img
class="w-fit h-48 lg:h-96 rounded" class="w-fit h-48 lg:h-96 rounded"
:src="useObject(image)" :src="useObject(image)"
/> />
</VueSlide> </VueSlide>
<VueSlide v-if="game.mImageCarousel.length == 0"> <VueSlide v-if="game.mImageCarouselObjectIds.length == 0">
<div <div
class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display" class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display"
> >
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<div class="absolute inset-0"> <div class="absolute inset-0">
<img <img
:src="useObject(game.mBannerId)" :src="useObject(game.mBannerObjectId)"
alt="" alt=""
class="size-full object-cover object-center" class="size-full object-cover object-center"
/> />
@@ -0,0 +1,10 @@
-- Rename game table columns
ALTER TABLE "Game" RENAME COLUMN "mIconId" TO "mIconObjectId";
ALTER TABLE "Game" RENAME COLUMN "mBannerId" TO "mBannerObjectId";
ALTER TABLE "Game" RENAME COLUMN "mCoverId" TO "mCoverObjectId";
ALTER TABLE "Game" RENAME COLUMN "mImageCarousel" TO "mImageCarouselObjectIds";
ALTER TABLE "Game" RENAME COLUMN "mImageLibrary" TO "mImageLibraryObjectIds";
-- Rename saveslot table columns
ALTER TABLE "SaveSlot" RENAME COLUMN "history" TO "historyObjectIds";
+6 -6
View File
@@ -24,11 +24,11 @@ model Game {
mReviewCount Int mReviewCount Int
mReviewRating Float // 0 to 1 mReviewRating Float // 0 to 1
mIconId String // linked to objects in s3 mIconObjectId String // linked to objects in s3
mBannerId String // linked to objects in s3 mBannerObjectId String // linked to objects in s3
mCoverId String mCoverObjectId String
mImageCarousel String[] // linked to below array mImageCarouselObjectIds String[] // linked to below array
mImageLibrary String[] // linked to objects in s3 mImageLibraryObjectIds String[] // linked to objects in s3
versions GameVersion[] versions GameVersion[]
libraryBasePath String @unique // Base dir for all the game versions libraryBasePath String @unique // Base dir for all the game versions
@@ -80,7 +80,7 @@ model SaveSlot {
lastUsedClientId String? lastUsedClientId String?
lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id]) lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id])
history String[] // list of objects historyObjectIds String[] // list of objects
historyChecksums String[] // list of hashes historyChecksums String[] // list of hashes
@@id([gameId, userId, index], name: "id") @@id([gameId, userId, index], name: "id")
@@ -21,27 +21,27 @@ export default defineEventHandler(async (h3) => {
id: gameId, id: gameId,
}, },
select: { select: {
mBannerId: true, mBannerObjectId: true,
mImageLibrary: true, mImageLibraryObjectIds: true,
mCoverId: true, mCoverObjectId: true,
}, },
}); });
if (!game) if (!game)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
const imageIndex = game.mImageLibrary.findIndex((e) => e == imageId); const imageIndex = game.mImageLibraryObjectIds.findIndex((e) => e == imageId);
if (imageIndex == -1) if (imageIndex == -1)
throw createError({ statusCode: 400, statusMessage: "Image not found" }); throw createError({ statusCode: 400, statusMessage: "Image not found" });
game.mImageLibrary.splice(imageIndex, 1); game.mImageLibraryObjectIds.splice(imageIndex, 1);
await objectHandler.deleteAsSystem(imageId); await objectHandler.deleteAsSystem(imageId);
if (game.mBannerId === imageId) { if (game.mBannerObjectId === imageId) {
game.mBannerId = game.mImageLibrary[0]; game.mBannerObjectId = game.mImageLibraryObjectIds[0];
} }
if (game.mCoverId === imageId) { if (game.mCoverObjectId === imageId) {
game.mCoverId = game.mImageLibrary[0]; game.mCoverObjectId = game.mImageLibraryObjectIds[0];
} }
const result = await prisma.game.update({ const result = await prisma.game.update({
@@ -49,14 +49,14 @@ export default defineEventHandler(async (h3) => {
id: gameId, id: gameId,
}, },
data: { data: {
mBannerId: game.mBannerId, mBannerObjectId: game.mBannerObjectId,
mImageLibrary: game.mImageLibrary, mImageLibraryObjectIds: game.mImageLibraryObjectIds,
mCoverId: game.mCoverId, mCoverObjectId: game.mCoverObjectId,
}, },
select: { select: {
mBannerId: true, mBannerObjectId: true,
mImageLibrary: true, mImageLibraryObjectIds: true,
mCoverId: true, mCoverObjectId: true,
}, },
}); });
@@ -47,7 +47,7 @@ export default defineEventHandler(async (h3) => {
id: gameId, id: gameId,
}, },
data: { data: {
mImageLibrary: { mImageLibraryObjectIds: {
push: id, push: id,
}, },
}, },
@@ -42,7 +42,7 @@ export default defineEventHandler(async (h3) => {
id: gameId, id: gameId,
}, },
data: { data: {
mIconId: id, mIconObjectId: id,
mName: name, mName: name,
mShortDescription: description, mShortDescription: description,
}, },
+2 -2
View File
@@ -10,8 +10,8 @@ export default defineEventHandler(async (h3) => {
id: true, id: true,
mName: true, mName: true,
mShortDescription: true, mShortDescription: true,
mCoverId: true, mCoverObjectId: true,
mBannerId: true, mBannerObjectId: true,
mDevelopers: { mDevelopers: {
select: { select: {
id: true, id: true,
+1 -1
View File
@@ -73,7 +73,7 @@ class LibraryManager {
metadataSource: true, metadataSource: true,
mDevelopers: true, mDevelopers: true,
mPublishers: true, mPublishers: true,
mIconId: true, mIconObjectId: true,
libraryBasePath: true, libraryBasePath: true,
}, },
orderBy: { orderBy: {
+4 -4
View File
@@ -174,10 +174,10 @@ export class MetadataHandler {
mReviewRating: metadata.reviewRating, mReviewRating: metadata.reviewRating,
mReleased: metadata.released, mReleased: metadata.released,
mIconId: metadata.icon, mIconObjectId: metadata.icon,
mBannerId: metadata.bannerId, mBannerObjectId: metadata.bannerId,
mCoverId: metadata.coverId, mCoverObjectId: metadata.coverId,
mImageLibrary: metadata.images, mImageLibraryObjectIds: metadata.images,
libraryBasePath, libraryBasePath,
}, },
+6 -6
View File
@@ -77,7 +77,7 @@ class SaveManager {
}, },
}, },
data: { data: {
history: { historyObjectIds: {
push: newSaveObjectId, push: newSaveObjectId,
}, },
historyChecksums: { historyChecksums: {
@@ -88,12 +88,12 @@ class SaveManager {
}); });
const historyLimit = await applicationSettings.get("saveSlotHistoryLimit"); const historyLimit = await applicationSettings.get("saveSlotHistoryLimit");
if (newSave.history.length > historyLimit) { if (newSave.historyObjectIds.length > historyLimit) {
// Delete previous // Delete previous
const safeFromIndex = newSave.history.length - historyLimit; const safeFromIndex = newSave.historyObjectIds.length - historyLimit;
const toDelete = newSave.history.slice(0, safeFromIndex); const toDelete = newSave.historyObjectIds.slice(0, safeFromIndex);
const toKeepObjects = newSave.history.slice(safeFromIndex); const toKeepObjects = newSave.historyObjectIds.slice(safeFromIndex);
const toKeepHashes = newSave.historyChecksums.slice(safeFromIndex); const toKeepHashes = newSave.historyChecksums.slice(safeFromIndex);
// Delete objects first, so if we error out, we don't lose track of objects in backend // Delete objects first, so if we error out, we don't lose track of objects in backend
@@ -110,7 +110,7 @@ class SaveManager {
}, },
}, },
data: { data: {
history: toKeepObjects, historyObjectIds: toKeepObjects,
historyChecksums: toKeepHashes, historyChecksums: toKeepHashes,
}, },
}); });