mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
feat: add RefreshMetadataDialog with scan type and metadata source selection
Agent-Logs-Url: https://github.com/rommapp/romm/sessions/fec651c3-d741-4c7d-a79f-bd469343602e Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ef94de8a28
commit
b9d784d3b4
@@ -4,13 +4,11 @@ import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useFavoriteToggle } from "@/composables/useFavoriteToggle";
|
||||
import romApi from "@/services/api/rom";
|
||||
import socket from "@/services/socket";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import type { SimpleRom } from "@/stores/roms";
|
||||
import storeScanning from "@/stores/scanning";
|
||||
import type { Events } from "@/types/emitter";
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -21,7 +19,6 @@ const auth = storeAuth();
|
||||
const collectionsStore = storeCollections();
|
||||
const { toggleFavorite } = useFavoriteToggle(emitter);
|
||||
const romsStore = storeRoms();
|
||||
const scanningStore = storeScanning();
|
||||
|
||||
async function switchFromFavorites() {
|
||||
await toggleFavorite(props.rom);
|
||||
@@ -55,22 +52,6 @@ async function resetLastPlayed() {
|
||||
});
|
||||
}
|
||||
|
||||
async function onScan() {
|
||||
scanningStore.setScanning(true);
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Refreshing ${props.rom.name} metadata...`,
|
||||
icon: "mdi-loading mdi-spin",
|
||||
color: "primary",
|
||||
});
|
||||
|
||||
if (!socket.connected) socket.connect();
|
||||
socket.emit("scan", {
|
||||
platforms: [props.rom.platform_id],
|
||||
roms_ids: [props.rom.id],
|
||||
type: "quick", // Quick scan so we can filter by selected roms
|
||||
apis: heartbeat.getAllMetadataOptions().map((s) => s.value),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -102,7 +83,7 @@ async function onScan() {
|
||||
<v-icon icon="mdi-pencil-box" class="mr-2" />{{ t("common.edit") }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="py-4 pr-5" @click="onScan">
|
||||
<v-list-item class="py-4 pr-5" @click="emitter?.emit('showRefreshMetadataDialog', rom)">
|
||||
<v-list-item-title class="d-flex">
|
||||
<v-icon icon="mdi-magnify-scan" class="mr-2" />{{
|
||||
t("rom.refresh-metadata")
|
||||
|
||||
297
frontend/src/components/common/Game/Dialog/RefreshMetadata.vue
Normal file
297
frontend/src/components/common/Game/Dialog/RefreshMetadata.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { storeToRefs } from "pinia";
|
||||
import RDialog from "@/components/common/RDialog.vue";
|
||||
import socket from "@/services/socket";
|
||||
import storeConfig from "@/stores/config";
|
||||
import storeHeartbeat, { type MetadataOption } from "@/stores/heartbeat";
|
||||
import { type SimpleRom } from "@/stores/roms";
|
||||
import storeScanning from "@/stores/scanning";
|
||||
import type { Events } from "@/types/emitter";
|
||||
|
||||
const LOCAL_STORAGE_METADATA_SOURCES_KEY = "scan.metadataSources";
|
||||
const LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY =
|
||||
"scan.launchboxRemoteEnabled";
|
||||
|
||||
const { t } = useI18n();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const show = ref(false);
|
||||
const rom = ref<SimpleRom | null>(null);
|
||||
const heartbeat = storeHeartbeat();
|
||||
const scanningStore = storeScanning();
|
||||
const configStore = storeConfig();
|
||||
const { config } = storeToRefs(configStore);
|
||||
|
||||
const calculateHashes = computed(() => !config.value.SKIP_HASH_CALCULATION || false);
|
||||
|
||||
const metadataOptions = computed(() => {
|
||||
return heartbeat.getMetadataOptionsByPriority().map((option) => {
|
||||
const requiresHashes = option.value === "hasheous" || option.value === "ra";
|
||||
const hashingDisabled = !calculateHashes.value;
|
||||
|
||||
let disabled = option.disabled;
|
||||
|
||||
if (hashingDisabled && requiresHashes) {
|
||||
if (option.value === "hasheous") {
|
||||
disabled = t("scan.hasheous-requires-hashes");
|
||||
} else if (option.value === "ra") {
|
||||
disabled = t("scan.retroachievements-requires-hashes");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const storedMetadataSources = useLocalStorage(
|
||||
LOCAL_STORAGE_METADATA_SOURCES_KEY,
|
||||
[] as string[],
|
||||
);
|
||||
const launchboxRemoteEnabled = useLocalStorage(
|
||||
LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY,
|
||||
true,
|
||||
);
|
||||
|
||||
const metadataSources = ref<MetadataOption[]>(
|
||||
metadataOptions.value.filter(
|
||||
(m) => storedMetadataSources.value.includes(m.value) && !m.disabled,
|
||||
) || heartbeat.getEnabledMetadataOptions(),
|
||||
);
|
||||
|
||||
const isLaunchboxSelected = computed(() =>
|
||||
metadataSources.value.some((s) => s.value === "launchbox"),
|
||||
);
|
||||
|
||||
watch(metadataOptions, (newOptions) => {
|
||||
metadataSources.value = metadataSources.value.filter((s) =>
|
||||
newOptions.some((opt) => opt.value === s.value && !opt.disabled),
|
||||
);
|
||||
});
|
||||
|
||||
const scanOptions = computed(() => [
|
||||
{
|
||||
title: t("scan.quick-scan"),
|
||||
subtitle: t("scan.quick-scan-desc"),
|
||||
value: "quick",
|
||||
},
|
||||
{
|
||||
title: t("scan.unmatched-games"),
|
||||
subtitle: t("scan.unmatched-games-desc"),
|
||||
value: "unmatched",
|
||||
},
|
||||
{
|
||||
title: t("scan.update-metadata"),
|
||||
subtitle: t("scan.update-metadata-desc"),
|
||||
value: "update",
|
||||
},
|
||||
{
|
||||
title: t("scan.complete-rescan"),
|
||||
subtitle: t("scan.complete-rescan-desc"),
|
||||
value: "complete",
|
||||
},
|
||||
]);
|
||||
const scanType = ref("quick");
|
||||
|
||||
const handleShowRefreshMetadataDialog = (romToRefresh: SimpleRom) => {
|
||||
rom.value = romToRefresh;
|
||||
show.value = true;
|
||||
};
|
||||
emitter?.on("showRefreshMetadataDialog", handleShowRefreshMetadataDialog);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emitter?.off("showRefreshMetadataDialog", handleShowRefreshMetadataDialog);
|
||||
});
|
||||
|
||||
async function onScan() {
|
||||
if (!rom.value) return;
|
||||
|
||||
scanningStore.setScanning(true);
|
||||
|
||||
// Store selected meta sources in storage
|
||||
storedMetadataSources.value = metadataSources.value.map((s) => s.value);
|
||||
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Refreshing ${rom.value.name ?? rom.value.fs_name} metadata...`,
|
||||
icon: "mdi-loading mdi-spin",
|
||||
color: "primary",
|
||||
});
|
||||
|
||||
if (!socket.connected) socket.connect();
|
||||
socket.emit("scan", {
|
||||
platforms: [rom.value.platform_id],
|
||||
roms_ids: [rom.value.id],
|
||||
type: scanType.value,
|
||||
apis: metadataSources.value.map((s) => s.value),
|
||||
launchbox_remote_enabled: launchboxRemoteEnabled.value,
|
||||
});
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
show.value = false;
|
||||
rom.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RDialog
|
||||
v-model="show"
|
||||
icon="mdi-magnify-scan"
|
||||
:width="'500px'"
|
||||
@close="closeDialog"
|
||||
>
|
||||
<template #header>
|
||||
<v-toolbar-title class="text-h6 ml-2">
|
||||
{{ t("rom.refresh-metadata") }}
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<v-row class="pa-4" no-gutters>
|
||||
<v-col cols="12" class="mb-4">
|
||||
<span class="text-body-2 text-medium-emphasis">
|
||||
{{ rom?.name ?? rom?.fs_name }}
|
||||
</span>
|
||||
</v-col>
|
||||
|
||||
<!-- Metadata sources -->
|
||||
<v-col cols="12" class="mb-4">
|
||||
<v-select
|
||||
v-model="metadataSources"
|
||||
:items="metadataOptions"
|
||||
:label="t('scan.metadata-sources')"
|
||||
item-title="name"
|
||||
prepend-inner-icon="mdi-database-search"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
multiple
|
||||
return-object
|
||||
clearable
|
||||
hide-details
|
||||
chips
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
:title="item.raw.name"
|
||||
:subtitle="item.raw.disabled"
|
||||
:disabled="Boolean(item.raw.disabled)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="25" rounded="1">
|
||||
<v-img :src="item.raw.logo_path" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="item.raw.value === 'launchbox'"
|
||||
#append
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<span
|
||||
class="text-caption text-primary text-medium-emphasis mr-4"
|
||||
:class="{ 'text-romm-gray': launchboxRemoteEnabled }"
|
||||
>
|
||||
Local
|
||||
</span>
|
||||
<v-switch
|
||||
v-model="launchboxRemoteEnabled"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="!isLaunchboxSelected"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
<span
|
||||
class="text-caption text-primary text-medium-emphasis ml-4"
|
||||
:class="{ 'text-romm-gray': !launchboxRemoteEnabled }"
|
||||
>
|
||||
Cloud
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #chip="{ item }">
|
||||
<v-avatar class="mx-1" size="24" rounded="1">
|
||||
<v-img :src="item.raw.logo_path" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<!-- Scan type -->
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="scanType"
|
||||
:items="scanOptions"
|
||||
:label="t('scan.scan-options')"
|
||||
prepend-inner-icon="mdi-magnify-scan"
|
||||
hide-details
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item v-bind="props" :subtitle="item.raw.subtitle" />
|
||||
</template>
|
||||
<template #append-inner>
|
||||
<v-menu open-on-hover location="bottom start">
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-card max-width="600">
|
||||
<v-card-text>
|
||||
<div v-html="t('scan.scan-types-info')" />
|
||||
<div class="mt-3 text-right">
|
||||
<a
|
||||
href="https://docs.romm.app/latest/Usage/LibraryManagement/#scan"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="font-style: italic; text-decoration: underline"
|
||||
>
|
||||
{{ t("scan.scan-types-more-info") }}
|
||||
</a>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<v-row class="justify-end pa-2" no-gutters>
|
||||
<v-btn variant="text" @click="closeDialog">
|
||||
{{ t("common.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="metadataSources.length === 0"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
@click="onScan"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-magnify-scan" />
|
||||
</template>
|
||||
{{ t("rom.refresh-metadata") }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</template>
|
||||
</RDialog>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@ import DeleteRomDialog from "@/components/common/Game/Dialog/DeleteRom.vue";
|
||||
import EditRomDialog from "@/components/common/Game/Dialog/EditRom.vue";
|
||||
import MatchRomDialog from "@/components/common/Game/Dialog/MatchRom.vue";
|
||||
import NoteDialog from "@/components/common/Game/Dialog/NoteDialog.vue";
|
||||
import RefreshMetadataDialog from "@/components/common/Game/Dialog/RefreshMetadata.vue";
|
||||
import ShowQRCodeDialog from "@/components/common/Game/Dialog/ShowQRCode.vue";
|
||||
import MainAppBar from "@/components/common/Navigation/MainAppBar.vue";
|
||||
import NewVersionDialog from "@/components/common/NewVersionDialog.vue";
|
||||
@@ -76,6 +77,7 @@ onBeforeMount(async () => {
|
||||
<LoadingDialog />
|
||||
<MatchRomDialog />
|
||||
<EditRomDialog />
|
||||
<RefreshMetadataDialog />
|
||||
<SearchCoverDialog />
|
||||
<AddRomsToCollectionDialog />
|
||||
<RemoveRomsFromCollectionDialog />
|
||||
|
||||
1
frontend/src/types/emitter.d.ts
vendored
1
frontend/src/types/emitter.d.ts
vendored
@@ -25,6 +25,7 @@ export type Events = {
|
||||
showSearchCoverDialog: { term: string; platformId?: number };
|
||||
updateUrlCover: string;
|
||||
showEditRomDialog: SimpleRom;
|
||||
showRefreshMetadataDialog: SimpleRom;
|
||||
showCopyDownloadLinkDialog: string;
|
||||
showDeleteRomDialog: SimpleRom[];
|
||||
showUploadRomDialog: Platform | null;
|
||||
|
||||
Reference in New Issue
Block a user