mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
* #51 Adds settings page with showTitleDescriptionOnGamePanel * Removes console.log * Renames isHidden to system, adds missing system column on Game and fixes nitro plugin on fresh database * Implements a different way to handle the placeholder image * Removes system column on Game * Groups settings keys together * Removes unused i18n keys * fix: fix eslints and other small tweaks --------- Co-authored-by: Francois Ribemont <ribemont.francois@gmail.com>
This commit is contained in:
@@ -7,7 +7,11 @@
|
|||||||
:key="gameIdx"
|
:key="gameIdx"
|
||||||
class="justify-start"
|
class="justify-start"
|
||||||
>
|
>
|
||||||
<GamePanel :game="game" />
|
<GamePanel
|
||||||
|
:game="game"
|
||||||
|
:href="game ? `/store/${game.id}` : undefined"
|
||||||
|
:show-title-description="showGamePanelTextDecoration"
|
||||||
|
/>
|
||||||
</VueSlide>
|
</VueSlide>
|
||||||
|
|
||||||
<template #addons>
|
<template #addons>
|
||||||
@@ -40,6 +44,10 @@ const props = defineProps<{
|
|||||||
width?: number;
|
width?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { showGamePanelTextDecoration } = await $dropFetch(
|
||||||
|
`/api/v1/admin/settings`,
|
||||||
|
);
|
||||||
|
|
||||||
const currentComponent = ref<HTMLDivElement>();
|
const currentComponent = ref<HTMLDivElement>();
|
||||||
|
|
||||||
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
||||||
|
|||||||
+57
-17
@@ -1,44 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="game"
|
v-if="game || defaultPlaceholder"
|
||||||
:href="props.href ?? `/store/${game.id}`"
|
:href="href"
|
||||||
class="group relative w-48 h-64 rounded-lg overflow-hidden transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5"
|
:class="{
|
||||||
@click="active = game.id"
|
'transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5':
|
||||||
|
animate,
|
||||||
|
}"
|
||||||
|
class="group relative w-48 h-64 rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 transition-all duration-300 group-hover:scale-110"
|
:class="{
|
||||||
|
'transition-all duration-300 group-hover:scale-110': animate,
|
||||||
|
}"
|
||||||
|
class="absolute inset-0"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="useObject(game.mCoverObjectId)"
|
:src="imageProps.src"
|
||||||
class="w-full h-full object-cover brightness-[90%]"
|
class="w-full h-full object-cover brightness-[90%]"
|
||||||
:class="{ active: active === game.id }"
|
:alt="imageProps.alt"
|
||||||
:alt="game.mName"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-gradient-to-t from-zinc-950/80 via-zinc-950/20 to-transparent"
|
class="absolute inset-0 bg-gradient-to-t from-zinc-950/80 via-zinc-950/20 to-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full p-3">
|
<div
|
||||||
|
v-if="showTitleDescription"
|
||||||
|
class="absolute bottom-0 left-0 w-full p-3"
|
||||||
|
>
|
||||||
<h1
|
<h1
|
||||||
class="text-zinc-100 text-sm font-bold font-display group-hover:text-white transition-colors"
|
:class="{ 'group-hover:text-white transition-colors': animate }"
|
||||||
|
class="text-zinc-100 text-sm font-bold font-display"
|
||||||
>
|
>
|
||||||
{{ game.mName }}
|
{{ game ? game.mName : $t("settings.admin.store.dropGameNamePlaceholder") }}
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
class="text-zinc-400 text-xs line-clamp-2 group-hover:text-zinc-300 transition-colors"
|
:class="{
|
||||||
|
'group-hover:text-zinc-300 transition-colors': animate,
|
||||||
|
}"
|
||||||
|
class="text-zinc-400 text-xs line-clamp-2"
|
||||||
>
|
>
|
||||||
{{ game.mShortDescription }}
|
{{
|
||||||
|
game
|
||||||
|
? game.mShortDescription
|
||||||
|
: $t("settings.admin.store.dropGameDescriptionPlaceholder")
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<SkeletonCard v-else :message="$t('store.noGame')" />
|
<SkeletonCard
|
||||||
|
v-else-if="defaultPlaceholder === false"
|
||||||
|
:message="$t('store.noGame')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
|
|
||||||
const props = defineProps<{
|
const { t } = useI18n();
|
||||||
|
const {
|
||||||
|
game,
|
||||||
|
href = undefined,
|
||||||
|
showTitleDescription = true,
|
||||||
|
animate = true,
|
||||||
|
defaultPlaceholder = false,
|
||||||
|
} = defineProps<{
|
||||||
game:
|
game:
|
||||||
| SerializeObject<{
|
| SerializeObject<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -46,11 +72,25 @@ const props = defineProps<{
|
|||||||
mName: string;
|
mName: string;
|
||||||
mShortDescription: string;
|
mShortDescription: string;
|
||||||
}>
|
}>
|
||||||
| undefined;
|
| undefined
|
||||||
|
| null;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
showTitleDescription?: boolean;
|
||||||
|
animate?: boolean;
|
||||||
|
defaultPlaceholder?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const active = useState();
|
const imageProps = {
|
||||||
|
src: "",
|
||||||
|
alt: t("settings.admin.store.dropGameAltPlaceholder"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
imageProps.src = useObject(game.mCoverObjectId);
|
||||||
|
imageProps.alt = game.mName;
|
||||||
|
} else if (defaultPlaceholder) {
|
||||||
|
imageProps.src = "/game-panel-placeholder.png";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'transition border border-3 rounded-xl relative cursor-pointer',
|
||||||
|
active ? 'border-blue-600' : 'border-zinc-700',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div v-if="active" class="absolute top-1 right-1 z-1">
|
||||||
|
<CheckIcon
|
||||||
|
class="rounded-full p-1.5 bg-blue-600 size-6 text-transparent stroke-3 stroke-zinc-900 font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CheckIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
const { active = false } = defineProps<{ active?: boolean }>();
|
||||||
|
</script>
|
||||||
+1
-1
@@ -13,7 +13,7 @@ export default withNuxt([
|
|||||||
// Optional.
|
// Optional.
|
||||||
"@intlify/vue-i18n/no-dynamic-keys": "error",
|
"@intlify/vue-i18n/no-dynamic-keys": "error",
|
||||||
"@intlify/vue-i18n/no-unused-keys": [
|
"@intlify/vue-i18n/no-unused-keys": [
|
||||||
"error",
|
"off",
|
||||||
{
|
{
|
||||||
extensions: [".js", ".vue", ".ts"],
|
extensions: [".js", ".vue", ".ts"],
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-2
@@ -227,7 +227,8 @@
|
|||||||
"admin": {
|
"admin": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"users": "Users"
|
"users": "Users",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"openSidebar": "Open sidebar"
|
"openSidebar": "Open sidebar"
|
||||||
@@ -462,10 +463,24 @@
|
|||||||
},
|
},
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"saved": "Saved",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"insert": "Insert",
|
"insert": "Insert",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"settings": "Settings",
|
"settings": {
|
||||||
|
"admin": {
|
||||||
|
"title": "Settings",
|
||||||
|
"description": "Configure Drop settings",
|
||||||
|
|
||||||
|
"store": {
|
||||||
|
"title": "Store",
|
||||||
|
"showGamePanelTextDecoration": "Show title and description on game tiles (default: on)",
|
||||||
|
"dropGameNamePlaceholder": "Example Game",
|
||||||
|
"dropGameDescriptionPlaceholder": "This is an example game. It will be replaced if you import a game.",
|
||||||
|
"dropGameAltPlaceholder": "Example Game icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"store": {
|
"store": {
|
||||||
"commingSoon": "coming soon",
|
"commingSoon": "coming soon",
|
||||||
"exploreMore": "Explore more {arrow}",
|
"exploreMore": "Explore more {arrow}",
|
||||||
|
|||||||
+1
-1
@@ -193,7 +193,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
|||||||
icon: RectangleStackIcon,
|
icon: RectangleStackIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t("settings"),
|
label: $t("header.admin.settings"),
|
||||||
route: "/admin/settings",
|
route: "/admin/settings",
|
||||||
prefix: "/admin/settings",
|
prefix: "/admin/settings",
|
||||||
icon: Cog6ToothIcon,
|
icon: Cog6ToothIcon,
|
||||||
|
|||||||
@@ -1,12 +1,121 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-gray-100">{{ $t("todo") }}</div>
|
<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("settings.admin.title") }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-base text-zinc-400">
|
||||||
|
{{ $t("settings.admin.description") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="space-y-4" @submit.prevent="() => saveSettings()">
|
||||||
|
<div class="py-6 border-y border-zinc-700">
|
||||||
|
<h2 class="text-xl font-semibold text-zinc-100">
|
||||||
|
{{ $t("settings.admin.store.title") }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<h3 class="text-base font-medium text-zinc-400 mb-3 m-x-0">
|
||||||
|
{{ $t("settings.admin.store.showGamePanelTextDecoration") }}
|
||||||
|
</h3>
|
||||||
|
<ul class="flex gap-3">
|
||||||
|
<li class="inline-block">
|
||||||
|
<OptionWrapper
|
||||||
|
:active="showGamePanelTextDecoration"
|
||||||
|
@click="setShowTitleDescription(true)"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<GamePanel
|
||||||
|
:animate="false"
|
||||||
|
:game="game"
|
||||||
|
:default-placeholder="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</OptionWrapper>
|
||||||
|
</li>
|
||||||
|
<li class="inline-block">
|
||||||
|
<OptionWrapper
|
||||||
|
:active="!showGamePanelTextDecoration"
|
||||||
|
@click="setShowTitleDescription(false)"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<GamePanel
|
||||||
|
:game="game"
|
||||||
|
:show-title-description="false"
|
||||||
|
:animate="false"
|
||||||
|
:default-placeholder="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</OptionWrapper>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex w-full shadow-sm sm:w-auto"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="!allowSave"
|
||||||
|
>
|
||||||
|
{{ allowSave ? $t("save") : $t("saved") }}
|
||||||
|
</LoadingButton>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
useHead({
|
<script setup lang="ts">
|
||||||
title: "Settings",
|
import { FetchError } from "ofetch";
|
||||||
});
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: t("settings.admin.title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = await $dropFetch("/api/v1/admin/settings");
|
||||||
|
const { game } = await $dropFetch("/api/v1/admin/settings/dummy-data");
|
||||||
|
|
||||||
|
const allowSave = ref(false);
|
||||||
|
|
||||||
|
const showGamePanelTextDecoration = ref<boolean>(
|
||||||
|
settings.showGamePanelTextDecoration,
|
||||||
|
);
|
||||||
|
|
||||||
|
function setShowTitleDescription(value: boolean) {
|
||||||
|
showGamePanelTextDecoration.value = value;
|
||||||
|
allowSave.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saving = ref<boolean>(false);
|
||||||
|
async function saveSettings() {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await $dropFetch("/api/v1/admin/settings", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: {
|
||||||
|
showGamePanelTextDecoration: showGamePanelTextDecoration.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
createModal(
|
||||||
|
ModalType.Notification,
|
||||||
|
{
|
||||||
|
title: `Failed to save settings.`,
|
||||||
|
description:
|
||||||
|
e instanceof FetchError
|
||||||
|
? (e.statusMessage ?? e.message)
|
||||||
|
: (e as string).toString(),
|
||||||
|
},
|
||||||
|
(_, c) => c(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
saving.value = false;
|
||||||
|
allowSave.value = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ApplicationSettings" ADD COLUMN "showGamePanelTextDecoration" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -6,6 +6,8 @@ model ApplicationSettings {
|
|||||||
saveSlotCountLimit Int @default(5)
|
saveSlotCountLimit Int @default(5)
|
||||||
saveSlotSizeLimit Float @default(10) // MB
|
saveSlotSizeLimit Float @default(10) // MB
|
||||||
saveSlotHistoryLimit Int @default(3)
|
saveSlotHistoryLimit Int @default(3)
|
||||||
|
|
||||||
|
showGamePanelTextDecoration Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Platform {
|
enum Platform {
|
||||||
|
|||||||
+13
-13
@@ -1,22 +1,22 @@
|
|||||||
enum ClientCapabilities {
|
enum ClientCapabilities {
|
||||||
PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client
|
PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client
|
||||||
UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc)
|
UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc)
|
||||||
CloudSaves @map("cloudSaves") // ability to save to save slots
|
CloudSaves @map("cloudSaves") // ability to save to save slots
|
||||||
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
|
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
|
||||||
}
|
}
|
||||||
|
|
||||||
// References a device
|
// References a device
|
||||||
model Client {
|
model Client {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
capabilities ClientCapabilities[]
|
capabilities ClientCapabilities[]
|
||||||
|
|
||||||
name String
|
name String
|
||||||
platform Platform
|
platform Platform
|
||||||
lastConnected DateTime
|
lastConnected DateTime
|
||||||
|
|
||||||
lastAccessedSaves SaveSlot[]
|
lastAccessedSaves SaveSlot[]
|
||||||
tokens APIToken[]
|
tokens APIToken[]
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-108
@@ -1,172 +1,172 @@
|
|||||||
enum MetadataSource {
|
enum MetadataSource {
|
||||||
Manual
|
Manual
|
||||||
GiantBomb
|
GiantBomb
|
||||||
PCGamingWiki
|
PCGamingWiki
|
||||||
IGDB
|
IGDB
|
||||||
Metacritic
|
Metacritic
|
||||||
OpenCritic
|
OpenCritic
|
||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
metadataSource MetadataSource
|
metadataSource MetadataSource
|
||||||
metadataId String
|
metadataId String
|
||||||
created DateTime @default(now())
|
created DateTime @default(now())
|
||||||
|
|
||||||
// Any field prefixed with m is filled in from metadata
|
// Any field prefixed with m is filled in from metadata
|
||||||
// Acts as a cache so we can search and filter it
|
// Acts as a cache so we can search and filter it
|
||||||
mName String // Name of game
|
mName String // Name of game
|
||||||
mShortDescription String // Short description
|
mShortDescription String // Short description
|
||||||
mDescription String // Supports markdown
|
mDescription String // Supports markdown
|
||||||
mReleased DateTime // When the game was released
|
mReleased DateTime // When the game was released
|
||||||
|
|
||||||
ratings GameRating[]
|
ratings GameRating[]
|
||||||
|
|
||||||
mIconObjectId String // linked to objects in s3
|
mIconObjectId String // linked to objects in s3
|
||||||
mBannerObjectId String // linked to objects in s3
|
mBannerObjectId String // linked to objects in s3
|
||||||
mCoverObjectId String
|
mCoverObjectId String
|
||||||
mImageCarouselObjectIds String[] // linked to below array
|
mImageCarouselObjectIds String[] // linked to below array
|
||||||
mImageLibraryObjectIds String[] // linked to objects in s3
|
mImageLibraryObjectIds String[] // linked to objects in s3
|
||||||
|
|
||||||
versions GameVersion[]
|
versions GameVersion[]
|
||||||
|
|
||||||
// These fields will not be optional in the next version
|
// These fields will not be optional in the next version
|
||||||
// Any game without a library ID will be assigned one at startup, based on the defaults
|
// Any game without a library ID will be assigned one at startup, based on the defaults
|
||||||
libraryId String?
|
libraryId String?
|
||||||
library Library? @relation(fields: [libraryId], references: [id])
|
library Library? @relation(fields: [libraryId], references: [id])
|
||||||
libraryPath String
|
libraryPath String
|
||||||
|
|
||||||
collections CollectionEntry[]
|
collections CollectionEntry[]
|
||||||
saves SaveSlot[]
|
saves SaveSlot[]
|
||||||
screenshots Screenshot[]
|
screenshots Screenshot[]
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
playtime Playtime[]
|
playtime Playtime[]
|
||||||
|
|
||||||
developers Company[] @relation(name: "developers")
|
developers Company[] @relation(name: "developers")
|
||||||
publishers Company[] @relation(name: "publishers")
|
publishers Company[] @relation(name: "publishers")
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
@@unique([libraryId, libraryPath], name: "libraryKey")
|
@@unique([libraryId, libraryPath], name: "libraryKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model GameRating {
|
model GameRating {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
metadataSource MetadataSource
|
metadataSource MetadataSource
|
||||||
metadataId String
|
metadataId String
|
||||||
created DateTime @default(now())
|
created DateTime @default(now())
|
||||||
|
|
||||||
mReviewCount Int
|
mReviewCount Int
|
||||||
mReviewRating Float // 0 to 1
|
mReviewRating Float // 0 to 1
|
||||||
|
|
||||||
mReviewHref String?
|
mReviewHref String?
|
||||||
|
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
gameId String
|
gameId String
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
// A particular set of files that relate to the version
|
// A particular set of files that relate to the version
|
||||||
model GameVersion {
|
model GameVersion {
|
||||||
gameId String
|
gameId String
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
versionName String // Sub directory for the game files
|
versionName String // Sub directory for the game files
|
||||||
|
|
||||||
created DateTime @default(now())
|
created DateTime @default(now())
|
||||||
|
|
||||||
platform Platform
|
platform Platform
|
||||||
|
|
||||||
launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
||||||
launchArgs String[]
|
launchArgs String[]
|
||||||
setupCommand String @default("") // Command to setup game (dependencies and such)
|
setupCommand String @default("") // Command to setup game (dependencies and such)
|
||||||
setupArgs String[]
|
setupArgs String[]
|
||||||
onlySetup Boolean @default(false)
|
onlySetup Boolean @default(false)
|
||||||
|
|
||||||
umuIdOverride String?
|
umuIdOverride String?
|
||||||
|
|
||||||
dropletManifest Json // Results from droplet
|
dropletManifest Json // Results from droplet
|
||||||
|
|
||||||
versionIndex Int
|
versionIndex Int
|
||||||
delta Boolean @default(false)
|
delta Boolean @default(false)
|
||||||
|
|
||||||
@@id([gameId, versionName])
|
@@id([gameId, versionName])
|
||||||
}
|
}
|
||||||
|
|
||||||
// A save slot for a game
|
// A save slot for a game
|
||||||
model SaveSlot {
|
model SaveSlot {
|
||||||
gameId String
|
gameId String
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
index Int
|
index Int
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
playtime Float @default(0) // hours
|
playtime Float @default(0) // hours
|
||||||
|
|
||||||
lastUsedClientId String?
|
lastUsedClientId String?
|
||||||
lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id])
|
lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id])
|
||||||
|
|
||||||
historyObjectIds 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Screenshot {
|
model Screenshot {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
gameId String
|
gameId String
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
objectId String
|
objectId String
|
||||||
private Boolean // if other users can see
|
private Boolean // if other users can see
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(0)
|
createdAt DateTime @default(now()) @db.Timestamptz(0)
|
||||||
|
|
||||||
@@index([gameId, userId])
|
@@index([gameId, userId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Playtime {
|
model Playtime {
|
||||||
gameId String
|
gameId String
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
seconds Int // seconds user has spent playing the game
|
seconds Int // seconds user has spent playing the game
|
||||||
|
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
|
||||||
@@id([gameId, userId])
|
@@id([gameId, userId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Company {
|
model Company {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
|
||||||
metadataSource MetadataSource
|
metadataSource MetadataSource
|
||||||
metadataId String
|
metadataId String
|
||||||
metadataOriginalQuery String
|
metadataOriginalQuery String
|
||||||
|
|
||||||
mName String
|
mName String
|
||||||
mShortDescription String
|
mShortDescription String
|
||||||
mDescription String
|
mDescription String
|
||||||
mLogoObjectId String
|
mLogoObjectId String
|
||||||
mBannerObjectId String
|
mBannerObjectId String
|
||||||
mWebsite String
|
mWebsite String
|
||||||
|
|
||||||
developed Game[] @relation(name: "developers")
|
developed Game[] @relation(name: "developers")
|
||||||
published Game[] @relation(name: "publishers")
|
published Game[] @relation(name: "publishers")
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model ObjectHash {
|
model ObjectHash {
|
||||||
id String @id
|
id String @id
|
||||||
hash String
|
hash String
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.getUserACL(h3, ["settings:read"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const game = await prisma.game.findFirst();
|
||||||
|
|
||||||
|
return { game };
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.getUserACL(h3, ["settings:read"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const showGamePanelTextDecoration = await applicationSettings.get(
|
||||||
|
"showGamePanelTextDecoration",
|
||||||
|
);
|
||||||
|
|
||||||
|
return { showGamePanelTextDecoration };
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { type } from "arktype";
|
||||||
|
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
||||||
|
import { readDropValidatedBody } from "~/server/arktype";
|
||||||
|
import { defineEventHandler, createError } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
|
||||||
|
const UpdateSettings = type({
|
||||||
|
showGamePanelTextDecoration: "boolean",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler<{ body: typeof UpdateSettings.infer }>(
|
||||||
|
async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["settings:update"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const body = await readDropValidatedBody(h3, UpdateSettings);
|
||||||
|
|
||||||
|
await applicationSettings.set(
|
||||||
|
"showGamePanelTextDecoration",
|
||||||
|
body.showGamePanelTextDecoration,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -39,6 +39,8 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
|||||||
"clients:revoke": "",
|
"clients:revoke": "",
|
||||||
|
|
||||||
"news:read": "Read the server's news articles.",
|
"news:read": "Read the server's news articles.",
|
||||||
|
|
||||||
|
"settings:read": "Read system settings.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||||
@@ -87,4 +89,6 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
|||||||
|
|
||||||
"maintenance:read":
|
"maintenance:read":
|
||||||
"Read tasks and maintenance information, like updates available and cleanup.",
|
"Read tasks and maintenance information, like updates available and cleanup.",
|
||||||
|
|
||||||
|
"settings:update": "Update system settings.",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export const userACLs = [
|
|||||||
"clients:revoke",
|
"clients:revoke",
|
||||||
|
|
||||||
"news:read",
|
"news:read",
|
||||||
|
|
||||||
|
"settings:read",
|
||||||
] as const;
|
] as const;
|
||||||
const userACLPrefix = "user:";
|
const userACLPrefix = "user:";
|
||||||
|
|
||||||
@@ -80,6 +82,8 @@ export const systemACLs = [
|
|||||||
"task:start",
|
"task:start",
|
||||||
|
|
||||||
"maintenance:read",
|
"maintenance:read",
|
||||||
|
|
||||||
|
"settings:update",
|
||||||
] as const;
|
] as const;
|
||||||
const systemACLPrefix = "system:";
|
const systemACLPrefix = "system:";
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ class LibraryManager {
|
|||||||
// Checks are done in least to most expensive order
|
// Checks are done in least to most expensive order
|
||||||
async checkUnimportedGamePath(libraryId: string, libraryPath: string) {
|
async checkUnimportedGamePath(libraryId: string, libraryPath: string) {
|
||||||
const hasGame =
|
const hasGame =
|
||||||
(await prisma.game.count({ where: { libraryId, libraryPath } })) > 0;
|
(await prisma.game.count({
|
||||||
|
where: { libraryId, libraryPath },
|
||||||
|
})) > 0;
|
||||||
if (hasGame) return false;
|
if (hasGame) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user