mirror of
https://github.com/rommapp/romm.git
synced 2026-06-27 22:35:57 +00:00
feat: enhance ClientTokenAdminSchema and related components with user avatar and updated timestamp
This commit is contained in:
@@ -21,6 +21,8 @@ class ClientTokenCreateSchema(ClientTokenSchema):
|
||||
|
||||
class ClientTokenAdminSchema(ClientTokenSchema):
|
||||
username: str
|
||||
user_avatar_path: str
|
||||
user_updated_at: UTCDatetime
|
||||
|
||||
|
||||
class ClientTokenPairSchema(BaseModel):
|
||||
|
||||
@@ -105,6 +105,8 @@ def build_admin_schema(token: ClientToken) -> ClientTokenAdminSchema:
|
||||
created_at=token.created_at,
|
||||
user_id=token.user_id,
|
||||
username=token.user.username,
|
||||
user_avatar_path=token.user.avatar_path,
|
||||
user_updated_at=token.user.updated_at,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"mapping-types": "Mapping types",
|
||||
"metadata-catalogs": "General metadata",
|
||||
"metadata-get-key": "Get API key",
|
||||
"metadata-proxies": "Match proxies",
|
||||
"metadata-specialised": "Specialised sources",
|
||||
"metadata-subtitle-achievements": "Achievements",
|
||||
"metadata-subtitle-completion": "Completion times",
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface ClientTokenCreateSchema extends ClientTokenSchema {
|
||||
|
||||
export interface ClientTokenAdminSchema extends ClientTokenSchema {
|
||||
username: string;
|
||||
user_avatar_path: string;
|
||||
user_updated_at: string;
|
||||
}
|
||||
|
||||
export interface ClientTokenPairSchema {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useI18n } from "vue-i18n";
|
||||
import clientTokenApi, {
|
||||
type ClientTokenAdminSchema,
|
||||
} from "@/services/api/client-token";
|
||||
import { formatTimestamp } from "@/utils";
|
||||
import { defaultAvatarPath, formatTimestamp } from "@/utils";
|
||||
import ScopeCell from "@/v2/components/Settings/ScopeCell.vue";
|
||||
import { useConfirm } from "@/v2/composables/useConfirm";
|
||||
import { useSnackbar } from "@/v2/composables/useSnackbar";
|
||||
@@ -41,6 +41,12 @@ type SortKey = "username" | "name" | "expires_at" | "last_used_at";
|
||||
const sortKey = ref<SortKey>("username");
|
||||
const sortDir = ref<"asc" | "desc">("asc");
|
||||
|
||||
function avatarSrc(token: ClientTokenAdminSchema) {
|
||||
return token.user_avatar_path
|
||||
? `/assets/romm/assets/${token.user_avatar_path}?ts=${token.user_updated_at}`
|
||||
: defaultAvatarPath;
|
||||
}
|
||||
|
||||
function compareNullable(a: string | null, b: string | null, asc: boolean) {
|
||||
if (!a && !b) return 0;
|
||||
if (!a) return asc ? 1 : -1;
|
||||
@@ -54,15 +60,11 @@ const sortedTokens = computed(() => {
|
||||
list.sort((a, b) => {
|
||||
if (sortKey.value === "username") {
|
||||
return asc
|
||||
? a.username.localeCompare(b.username) ||
|
||||
a.name.localeCompare(b.name)
|
||||
: b.username.localeCompare(a.username) ||
|
||||
b.name.localeCompare(a.name);
|
||||
? a.username.localeCompare(b.username) || a.name.localeCompare(b.name)
|
||||
: b.username.localeCompare(a.username) || b.name.localeCompare(a.name);
|
||||
}
|
||||
if (sortKey.value === "name") {
|
||||
return asc
|
||||
? a.name.localeCompare(b.name)
|
||||
: b.name.localeCompare(a.name);
|
||||
return asc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
}
|
||||
return compareNullable(a[sortKey.value], b[sortKey.value], asc);
|
||||
});
|
||||
@@ -208,9 +210,14 @@ onMounted(fetchTokens);
|
||||
@update:sort="onSort"
|
||||
>
|
||||
<template #cell.username="{ row }">
|
||||
<span class="r-v2-admin-tokens__user">{{
|
||||
(row as ClientTokenAdminSchema).username
|
||||
}}</span>
|
||||
<div class="r-v2-admin-tokens__user">
|
||||
<img
|
||||
:src="avatarSrc(row as ClientTokenAdminSchema)"
|
||||
:alt="(row as ClientTokenAdminSchema).username"
|
||||
class="r-v2-admin-tokens__avatar"
|
||||
/>
|
||||
<span>{{ (row as ClientTokenAdminSchema).username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell.name="{ row }">
|
||||
<span class="r-v2-admin-tokens__name">{{
|
||||
@@ -272,11 +279,24 @@ onMounted(fetchTokens);
|
||||
}
|
||||
|
||||
.r-v2-admin-tokens__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: var(--r-font-weight-semibold);
|
||||
min-width: 0;
|
||||
}
|
||||
.r-v2-admin-tokens__user span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.r-v2-admin-tokens__avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.r-v2-admin-tokens__name {
|
||||
font-weight: var(--r-font-weight-medium);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// MetadataSources — v2-native rewrite. Provider tiles grouped by
|
||||
// category (game catalogs vs. specialised sources). Each tile shows:
|
||||
// MetadataSources — v2-native rewrite. Provider tiles grouped into
|
||||
// three categories (general catalogs, specialised sources, match
|
||||
// proxies) matching the split used in the Setup wizard. Each tile
|
||||
// shows:
|
||||
// • A circular logo
|
||||
// • Provider name + tone-coloured `RTag` status chip
|
||||
// (missing key / invalid key / connected / checking)
|
||||
@@ -30,6 +32,7 @@ const heartbeatStatus = ref<Record<string, boolean | undefined>>({
|
||||
flashpoint: undefined,
|
||||
hltb: undefined,
|
||||
sgdb: undefined,
|
||||
playmatch: undefined,
|
||||
});
|
||||
|
||||
type SourceStatus = "missing" | "invalid" | "ok" | "pending";
|
||||
@@ -58,15 +61,6 @@ const catalogs = computed<Source[]>(() => [
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.igdb,
|
||||
},
|
||||
{
|
||||
name: "MobyGames",
|
||||
value: "moby",
|
||||
logo: "/assets/scrappers/moby.png",
|
||||
website: "https://www.mobygames.com",
|
||||
docsUrl: "https://www.mobygames.com/info/api/",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.moby,
|
||||
},
|
||||
{
|
||||
name: "ScreenScraper",
|
||||
value: "ss",
|
||||
@@ -77,13 +71,13 @@ const catalogs = computed<Source[]>(() => [
|
||||
heartbeat: heartbeatStatus.value.ss,
|
||||
},
|
||||
{
|
||||
name: "Hasheous",
|
||||
value: "hasheous",
|
||||
logo: "/assets/scrappers/hasheous.png",
|
||||
website: "https://hasheous.org",
|
||||
docsUrl: "https://hasheous.org/index.html?page=apidocs",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.HASHEOUS_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.hasheous,
|
||||
name: "MobyGames",
|
||||
value: "moby",
|
||||
logo: "/assets/scrappers/moby.png",
|
||||
website: "https://www.mobygames.com",
|
||||
docsUrl: "https://www.mobygames.com/info/api/",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.moby,
|
||||
},
|
||||
{
|
||||
name: "LaunchBox",
|
||||
@@ -138,6 +132,27 @@ const specialised = computed<Source[]>(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const proxies = computed<Source[]>(() => [
|
||||
{
|
||||
name: "Hasheous",
|
||||
value: "hasheous",
|
||||
logo: "/assets/scrappers/hasheous.png",
|
||||
website: "https://hasheous.org",
|
||||
docsUrl: "https://hasheous.org/index.html?page=apidocs",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.HASHEOUS_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.hasheous,
|
||||
},
|
||||
{
|
||||
name: "PlayMatch",
|
||||
value: "playmatch",
|
||||
logo: "/assets/scrappers/playmatch.png",
|
||||
website: "https://github.com/RetroRealm/playmatch",
|
||||
docsUrl: "https://github.com/RetroRealm/playmatch",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.PLAYMATCH_API_ENABLED,
|
||||
heartbeat: heartbeatStatus.value.playmatch,
|
||||
},
|
||||
]);
|
||||
|
||||
function statusOf(source: Source): SourceStatus {
|
||||
if (source.disabled) return "missing";
|
||||
if (source.heartbeat === true) return "ok";
|
||||
@@ -168,7 +183,7 @@ function statusLabel(status: SourceStatus): string {
|
||||
}
|
||||
|
||||
async function fetchAllHeartbeats() {
|
||||
const all = [...catalogs.value, ...specialised.value];
|
||||
const all = [...catalogs.value, ...specialised.value, ...proxies.value];
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((source) => !source.disabled)
|
||||
@@ -300,6 +315,63 @@ onMounted(() => {
|
||||
</article>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
:title="t('settings.metadata-proxies')"
|
||||
icon="mdi-swap-horizontal-bold"
|
||||
>
|
||||
<div class="r-v2-meta__grid">
|
||||
<article
|
||||
v-for="source in proxies"
|
||||
:key="source.value"
|
||||
class="r-v2-meta__card"
|
||||
:class="{
|
||||
'r-v2-meta__card--missing': statusOf(source) === 'missing',
|
||||
}"
|
||||
>
|
||||
<header class="r-v2-meta__header">
|
||||
<div class="r-v2-meta__logo">
|
||||
<img :src="source.logo" :alt="source.name" />
|
||||
</div>
|
||||
<div class="r-v2-meta__head-text">
|
||||
<span class="r-v2-meta__name">{{ source.name }}</span>
|
||||
<span v-if="source.subtitle" class="r-v2-meta__subtitle">
|
||||
{{ source.subtitle }}
|
||||
</span>
|
||||
<RTag
|
||||
:tone="statusTone(statusOf(source))"
|
||||
:icon="statusIcon(statusOf(source))"
|
||||
:text="statusLabel(statusOf(source))"
|
||||
size="x-small"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="r-v2-meta__actions">
|
||||
<RBtn
|
||||
variant="translucent"
|
||||
size="small"
|
||||
prepend-icon="mdi-key-variant"
|
||||
:href="source.docsUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ t("settings.metadata-get-key") }}
|
||||
</RBtn>
|
||||
<RBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
:href="source.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ t("settings.metadata-website") }}
|
||||
</RBtn>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user