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"
|
||||
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));
|
||||
|
||||
+58
-18
@@ -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">
|
||||
<h1
|
||||
class="text-zinc-100 text-sm font-bold font-display group-hover:text-white transition-colors"
|
||||
<div
|
||||
v-if="showTitleDescription"
|
||||
class="absolute bottom-0 left-0 w-full p-3"
|
||||
>
|
||||
{{ game.mName }}
|
||||
<h1
|
||||
:class="{ 'group-hover:text-white transition-colors': animate }"
|
||||
class="text-zinc-100 text-sm font-bold font-display"
|
||||
>
|
||||
{{ 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>
|
||||
|
||||
@@ -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.
|
||||
"@intlify/vue-i18n/no-dynamic-keys": "error",
|
||||
"@intlify/vue-i18n/no-unused-keys": [
|
||||
"error",
|
||||
"off",
|
||||
{
|
||||
extensions: [".js", ".vue", ".ts"],
|
||||
},
|
||||
|
||||
+17
-2
@@ -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}",
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+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)
|
||||
saveSlotSizeLimit Float @default(10) // MB
|
||||
saveSlotHistoryLimit Int @default(3)
|
||||
|
||||
showGamePanelTextDecoration Boolean @default(true)
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
|
||||
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": "",
|
||||
|
||||
"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