mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +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"
|
||||
class="justify-start"
|
||||
>
|
||||
<GamePanel :game="game" />
|
||||
<GamePanel
|
||||
:game="game"
|
||||
:href="game ? `/store/${game.id}` : undefined"
|
||||
:show-title-description="showGamePanelTextDecoration"
|
||||
/>
|
||||
</VueSlide>
|
||||
|
||||
<template #addons>
|
||||
@ -40,6 +44,10 @@ const props = defineProps<{
|
||||
width?: number;
|
||||
}>();
|
||||
|
||||
const { showGamePanelTextDecoration } = await $dropFetch(
|
||||
`/api/v1/admin/settings`,
|
||||
);
|
||||
|
||||
const currentComponent = ref<HTMLDivElement>();
|
||||
|
||||
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
||||
|
||||
@ -1,44 +1,70 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-if="game"
|
||||
:href="props.href ?? `/store/${game.id}`"
|
||||
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"
|
||||
@click="active = game.id"
|
||||
v-if="game || defaultPlaceholder"
|
||||
:href="href"
|
||||
:class="{
|
||||
'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
|
||||
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
|
||||
:src="useObject(game.mCoverObjectId)"
|
||||
:src="imageProps.src"
|
||||
class="w-full h-full object-cover brightness-[90%]"
|
||||
:class="{ active: active === game.id }"
|
||||
:alt="game.mName"
|
||||
:alt="imageProps.alt"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-zinc-950/80 via-zinc-950/20 to-transparent"
|
||||
/>
|
||||
</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
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<SkeletonCard v-else :message="$t('store.noGame')" />
|
||||
<SkeletonCard
|
||||
v-else-if="defaultPlaceholder === false"
|
||||
:message="$t('store.noGame')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
game,
|
||||
href = undefined,
|
||||
showTitleDescription = true,
|
||||
animate = true,
|
||||
defaultPlaceholder = false,
|
||||
} = defineProps<{
|
||||
game:
|
||||
| SerializeObject<{
|
||||
id: string;
|
||||
@ -46,11 +72,25 @@ const props = defineProps<{
|
||||
mName: string;
|
||||
mShortDescription: string;
|
||||
}>
|
||||
| undefined;
|
||||
| undefined
|
||||
| null;
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
22
components/OptionWrapper.vue
Normal file
22
components/OptionWrapper.vue
Normal file
@ -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>
|
||||
@ -13,7 +13,7 @@ export default withNuxt([
|
||||
// Optional.
|
||||
"@intlify/vue-i18n/no-dynamic-keys": "error",
|
||||
"@intlify/vue-i18n/no-unused-keys": [
|
||||
"error",
|
||||
"off",
|
||||
{
|
||||
extensions: [".js", ".vue", ".ts"],
|
||||
},
|
||||
|
||||
@ -227,7 +227,8 @@
|
||||
"admin": {
|
||||
"admin": "Admin",
|
||||
"tasks": "Tasks",
|
||||
"users": "Users"
|
||||
"users": "Users",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"back": "Back",
|
||||
"openSidebar": "Open sidebar"
|
||||
@ -462,10 +463,24 @@
|
||||
},
|
||||
"options": "Options",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"add": "Add",
|
||||
"insert": "Insert",
|
||||
"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": {
|
||||
"commingSoon": "coming soon",
|
||||
"exploreMore": "Explore more {arrow}",
|
||||
|
||||
@ -193,7 +193,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
icon: RectangleStackIcon,
|
||||
},
|
||||
{
|
||||
label: $t("settings"),
|
||||
label: $t("header.admin.settings"),
|
||||
route: "/admin/settings",
|
||||
prefix: "/admin/settings",
|
||||
icon: Cog6ToothIcon,
|
||||
|
||||
@ -1,12 +1,121 @@
|
||||
<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>
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: "Settings",
|
||||
});
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FetchError } from "ofetch";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
definePageMeta({
|
||||
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>
|
||||
|
||||
@ -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)
|
||||
saveSlotSizeLimit Float @default(10) // MB
|
||||
saveSlotHistoryLimit Int @default(3)
|
||||
|
||||
showGamePanelTextDecoration Boolean @default(true)
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
enum ClientCapabilities {
|
||||
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)
|
||||
CloudSaves @map("cloudSaves") // ability to save to save slots
|
||||
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
|
||||
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)
|
||||
CloudSaves @map("cloudSaves") // ability to save to save slots
|
||||
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
|
||||
}
|
||||
|
||||
// References a device
|
||||
model Client {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
capabilities ClientCapabilities[]
|
||||
capabilities ClientCapabilities[]
|
||||
|
||||
name String
|
||||
platform Platform
|
||||
lastConnected DateTime
|
||||
name String
|
||||
platform Platform
|
||||
lastConnected DateTime
|
||||
|
||||
lastAccessedSaves SaveSlot[]
|
||||
tokens APIToken[]
|
||||
lastAccessedSaves SaveSlot[]
|
||||
tokens APIToken[]
|
||||
}
|
||||
|
||||
@ -1,172 +1,172 @@
|
||||
enum MetadataSource {
|
||||
Manual
|
||||
GiantBomb
|
||||
PCGamingWiki
|
||||
IGDB
|
||||
Metacritic
|
||||
OpenCritic
|
||||
Manual
|
||||
GiantBomb
|
||||
PCGamingWiki
|
||||
IGDB
|
||||
Metacritic
|
||||
OpenCritic
|
||||
}
|
||||
|
||||
model Game {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
created DateTime @default(now())
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
created DateTime @default(now())
|
||||
|
||||
// Any field prefixed with m is filled in from metadata
|
||||
// Acts as a cache so we can search and filter it
|
||||
mName String // Name of game
|
||||
mShortDescription String // Short description
|
||||
mDescription String // Supports markdown
|
||||
mReleased DateTime // When the game was released
|
||||
// Any field prefixed with m is filled in from metadata
|
||||
// Acts as a cache so we can search and filter it
|
||||
mName String // Name of game
|
||||
mShortDescription String // Short description
|
||||
mDescription String // Supports markdown
|
||||
mReleased DateTime // When the game was released
|
||||
|
||||
ratings GameRating[]
|
||||
ratings GameRating[]
|
||||
|
||||
mIconObjectId String // linked to objects in s3
|
||||
mBannerObjectId String // linked to objects in s3
|
||||
mCoverObjectId String
|
||||
mImageCarouselObjectIds String[] // linked to below array
|
||||
mImageLibraryObjectIds String[] // linked to objects in s3
|
||||
mIconObjectId String // linked to objects in s3
|
||||
mBannerObjectId String // linked to objects in s3
|
||||
mCoverObjectId String
|
||||
mImageCarouselObjectIds String[] // linked to below array
|
||||
mImageLibraryObjectIds String[] // linked to objects in s3
|
||||
|
||||
versions GameVersion[]
|
||||
versions GameVersion[]
|
||||
|
||||
// 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
|
||||
libraryId String?
|
||||
library Library? @relation(fields: [libraryId], references: [id])
|
||||
libraryPath String
|
||||
// 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
|
||||
libraryId String?
|
||||
library Library? @relation(fields: [libraryId], references: [id])
|
||||
libraryPath String
|
||||
|
||||
collections CollectionEntry[]
|
||||
saves SaveSlot[]
|
||||
screenshots Screenshot[]
|
||||
tags Tag[]
|
||||
playtime Playtime[]
|
||||
collections CollectionEntry[]
|
||||
saves SaveSlot[]
|
||||
screenshots Screenshot[]
|
||||
tags Tag[]
|
||||
playtime Playtime[]
|
||||
|
||||
developers Company[] @relation(name: "developers")
|
||||
publishers Company[] @relation(name: "publishers")
|
||||
developers Company[] @relation(name: "developers")
|
||||
publishers Company[] @relation(name: "publishers")
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
@@unique([libraryId, libraryPath], name: "libraryKey")
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
@@unique([libraryId, libraryPath], name: "libraryKey")
|
||||
}
|
||||
|
||||
model GameRating {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
created DateTime @default(now())
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
created DateTime @default(now())
|
||||
|
||||
mReviewCount Int
|
||||
mReviewRating Float // 0 to 1
|
||||
mReviewCount Int
|
||||
mReviewRating Float // 0 to 1
|
||||
|
||||
mReviewHref String?
|
||||
mReviewHref String?
|
||||
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
gameId String
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
}
|
||||
|
||||
// A particular set of files that relate to the version
|
||||
model GameVersion {
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
versionName String // Sub directory for the game files
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
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
|
||||
launchArgs String[]
|
||||
setupCommand String @default("") // Command to setup game (dependencies and such)
|
||||
setupArgs String[]
|
||||
onlySetup Boolean @default(false)
|
||||
launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
||||
launchArgs String[]
|
||||
setupCommand String @default("") // Command to setup game (dependencies and such)
|
||||
setupArgs String[]
|
||||
onlySetup Boolean @default(false)
|
||||
|
||||
umuIdOverride String?
|
||||
umuIdOverride String?
|
||||
|
||||
dropletManifest Json // Results from droplet
|
||||
dropletManifest Json // Results from droplet
|
||||
|
||||
versionIndex Int
|
||||
delta Boolean @default(false)
|
||||
versionIndex Int
|
||||
delta Boolean @default(false)
|
||||
|
||||
@@id([gameId, versionName])
|
||||
@@id([gameId, versionName])
|
||||
}
|
||||
|
||||
// A save slot for a game
|
||||
model SaveSlot {
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
index Int
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
index Int
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
playtime Float @default(0) // hours
|
||||
createdAt DateTime @default(now())
|
||||
playtime Float @default(0) // hours
|
||||
|
||||
lastUsedClientId String?
|
||||
lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id])
|
||||
lastUsedClientId String?
|
||||
lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id])
|
||||
|
||||
historyObjectIds String[] // list of objects
|
||||
historyChecksums String[] // list of hashes
|
||||
historyObjectIds String[] // list of objects
|
||||
historyChecksums String[] // list of hashes
|
||||
|
||||
@@id([gameId, userId, index], name: "id")
|
||||
@@id([gameId, userId, index], name: "id")
|
||||
}
|
||||
|
||||
model Screenshot {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
objectId String
|
||||
private Boolean // if other users can see
|
||||
objectId String
|
||||
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([userId])
|
||||
@@index([gameId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Playtime {
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
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)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
|
||||
@@id([gameId, userId])
|
||||
@@index([userId])
|
||||
@@id([gameId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Company {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
metadataOriginalQuery String
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
metadataOriginalQuery String
|
||||
|
||||
mName String
|
||||
mShortDescription String
|
||||
mDescription String
|
||||
mLogoObjectId String
|
||||
mBannerObjectId String
|
||||
mWebsite String
|
||||
mName String
|
||||
mShortDescription String
|
||||
mDescription String
|
||||
mLogoObjectId String
|
||||
mBannerObjectId String
|
||||
mWebsite String
|
||||
|
||||
developed Game[] @relation(name: "developers")
|
||||
published Game[] @relation(name: "publishers")
|
||||
developed Game[] @relation(name: "developers")
|
||||
published Game[] @relation(name: "publishers")
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
}
|
||||
|
||||
model ObjectHash {
|
||||
id String @id
|
||||
hash String
|
||||
id String @id
|
||||
hash String
|
||||
}
|
||||
|
||||
BIN
public/game-panel-placeholder.png
Normal file
BIN
public/game-panel-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
11
server/api/v1/admin/settings/dummy-data.get.ts
Normal file
11
server/api/v1/admin/settings/dummy-data.get.ts
Normal file
@ -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 };
|
||||
});
|
||||
13
server/api/v1/admin/settings/index.get.ts
Normal file
13
server/api/v1/admin/settings/index.get.ts
Normal file
@ -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 };
|
||||
});
|
||||
23
server/api/v1/admin/settings/index.patch.ts
Normal file
23
server/api/v1/admin/settings/index.patch.ts
Normal file
@ -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": "",
|
||||
|
||||
"news:read": "Read the server's news articles.",
|
||||
|
||||
"settings:read": "Read system settings.",
|
||||
};
|
||||
|
||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||
@ -87,4 +89,6 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||
|
||||
"maintenance:read":
|
||||
"Read tasks and maintenance information, like updates available and cleanup.",
|
||||
|
||||
"settings:update": "Update system settings.",
|
||||
};
|
||||
|
||||
@ -33,6 +33,8 @@ export const userACLs = [
|
||||
"clients:revoke",
|
||||
|
||||
"news:read",
|
||||
|
||||
"settings:read",
|
||||
] as const;
|
||||
const userACLPrefix = "user:";
|
||||
|
||||
@ -80,6 +82,8 @@ export const systemACLs = [
|
||||
"task:start",
|
||||
|
||||
"maintenance:read",
|
||||
|
||||
"settings:update",
|
||||
] as const;
|
||||
const systemACLPrefix = "system:";
|
||||
|
||||
|
||||
@ -172,7 +172,9 @@ class LibraryManager {
|
||||
// Checks are done in least to most expensive order
|
||||
async checkUnimportedGamePath(libraryId: string, libraryPath: string) {
|
||||
const hasGame =
|
||||
(await prisma.game.count({ where: { libraryId, libraryPath } })) > 0;
|
||||
(await prisma.game.count({
|
||||
where: { libraryId, libraryPath },
|
||||
})) > 0;
|
||||
if (hasGame) return false;
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user