Replaces the server/pages/community.vue stub ({{ $t("todo") }}) with real
Nuxt pages: a unified Drop+RomM title browser, an admin catalog-sync panel,
and a profile page with playtime rails. Corrects the original architecture
mistake of building this as a separate service at community.quasarke.net --
now that we own the fork's source, community is ordinary Nuxt pages instead
of a sidecar with its own auth/DB/vhost.
Data model: one new table, CommunityTitle, for the RomM catalog (ported
from drop-community M1's Title spine). Drop's own Game/User/Playtime models
already ARE the title/identity/playtime spine for Drop content, so nothing
new was needed there -- profile playtime rails read real, already-existing
Playtime rows, no wrapper/ingest milestone required to populate them.
RommAdapter (server/server/internal/community/rommAdapter.ts) reads RomM's
MariaDB directly via the community_ro read-only role, same deviation and
same reasoning as M1's adapter (RomM's /api/roms needs a browser session,
no service-account key). TitleSyncService runs every 6h
(scheduledTasks.communityTitleSync) and via an admin "Sync now" button
(system:community:sync ACL) -- never deletes, hides instead.
Also folds the three post-build JS patches
(homelab-compose/drop-stack/patch-{launcher-button,telemetry-snippet,
footer-links}.js) into source now that the fork builds from source instead
of patching upstream's prebuilt image:
- library/game/[id]/index.vue: the "Open in Launcher" button gets a real
@click handler (drop://open with a timeout fallback to /download).
- plugins/telemetry.client.ts: OpenPanel RUM injected via a Nuxt client
plugin instead of appending to the compiled entry chunk.
- UserFooter.vue + admin/library source-help links + error.vue + the
Weblate link: repointed at our own infra. The Discord/social link
used to point at community.quasarke.net; that service is retired, so
it now points at /community directly.
188 lines
6.4 KiB
Vue
188 lines
6.4 KiB
Vue
<template>
|
|
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
|
<table class="min-w-full divide-y divide-zinc-700">
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
scope="col"
|
|
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
|
|
>
|
|
{{ $t("common.name") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("type") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("library.admin.sources.working") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("options") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("library.admin.sources.totalSpace") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("library.admin.sources.freeSpace") }}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
|
>
|
|
{{ $t("library.admin.sources.utilizationPercentage") }}
|
|
</th>
|
|
<th
|
|
v-if="editSource || deleteSource"
|
|
scope="col"
|
|
class="relative py-3.5 pl-3 pr-4 sm:pr-3"
|
|
>
|
|
<span class="sr-only">{{ $t("actions") }}</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(source, sourceIdx) in sources" :key="source.id">
|
|
<td
|
|
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
|
|
>
|
|
{{ source.name }}
|
|
</td>
|
|
<td
|
|
class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400 flex gap-x-1 items-center"
|
|
>
|
|
<component
|
|
:is="optionsMetadata[source.backend].icon"
|
|
class="size-5 text-zinc-400"
|
|
/>
|
|
{{ optionsMetadata[source.backend].title }}
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
|
<CheckIcon v-if="source.working" class="size-5 text-green-500" />
|
|
<XMarkIcon v-else class="size-5 text-red-500" />
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
|
{{ source.options }}
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
|
{{ source.fsStats && formatBytes(source.fsStats.totalSpace) }}
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
|
{{ source.fsStats && formatBytes(source.fsStats.freeSpace) }}
|
|
</td>
|
|
<td
|
|
class="align-middle flex flex-cols-5 whitespace-nowrap px-3 py-4 text-sm text-zinc-400"
|
|
>
|
|
<div class="flex-auto content-right">
|
|
<ProgressBar
|
|
v-if="source.fsStats"
|
|
:percentage="
|
|
getPercentage(
|
|
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
|
source.fsStats.totalSpace,
|
|
)
|
|
"
|
|
:color="
|
|
getBarColor(
|
|
getPercentage(
|
|
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
|
source.fsStats.totalSpace,
|
|
),
|
|
)
|
|
"
|
|
background-color="slate"
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td
|
|
v-if="editSource || deleteSource"
|
|
class="relative whitespace-nowrap py-4 pl-3 pr-3 text-right text-sm font-medium space-x-2"
|
|
>
|
|
<button
|
|
v-if="editSource"
|
|
class="text-blue-500 hover:text-blue-400"
|
|
@click="() => editSource(sourceIdx)"
|
|
>
|
|
{{ $t("common.edit") }}
|
|
<span class="sr-only">
|
|
{{ $t("chars.srComma", [source.name]) }}
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
v-if="deleteSource"
|
|
class="text-red-500 hover:text-red-400"
|
|
@click="() => deleteSource(sourceIdx)"
|
|
>
|
|
{{ $t("common.delete") }}
|
|
<span class="sr-only">
|
|
{{ $t("chars.srComma", [source.name]) }}
|
|
</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
|
|
import type { LibraryBackend } from "~/prisma/client/enums";
|
|
import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
|
|
import { DropLogo } from "#components";
|
|
import { formatBytes } from "~/server/internal/utils/files";
|
|
import { getBarColor } from "~/utils/colors";
|
|
import { getPercentage } from "~/utils/utils";
|
|
|
|
const {
|
|
sources,
|
|
deleteSource = undefined,
|
|
editSource = undefined,
|
|
} = defineProps<{
|
|
sources: WorkingLibrarySource[];
|
|
summaryMode?: boolean;
|
|
deleteSource?: (id: number) => void;
|
|
editSource?: (id: number) => void;
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
|
|
const optionsMetadata: {
|
|
[key in LibraryBackend]: {
|
|
title: string;
|
|
description: string;
|
|
docsLink: string;
|
|
icon: Component;
|
|
};
|
|
} = {
|
|
Filesystem: {
|
|
title: t("library.admin.sources.fsTitle"),
|
|
description: t("library.admin.sources.fsDesc"),
|
|
docsLink: "https://drop-docs.quasarke.net/docs/reference/library-sources#drop-style",
|
|
icon: DropLogo,
|
|
},
|
|
FlatFilesystem: {
|
|
title: t("library.admin.sources.fsFlatTitle"),
|
|
description: t("library.admin.sources.fsFlatDesc"),
|
|
docsLink:
|
|
"https://drop-docs.quasarke.net/docs/reference/library-sources#compatibility-flat-style",
|
|
icon: BackwardIcon,
|
|
},
|
|
};
|
|
</script>
|