mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
feat: add platform game counts and enhance platform selection in Setup.vue
This commit is contained in:
@@ -172,6 +172,7 @@ async def get_setup_library_info():
|
||||
Returns:
|
||||
- detected_structure: "A" (roms/{platform}), "B" ({platform}/roms), or None
|
||||
- existing_platforms: list of platform fs_slugs already in filesystem
|
||||
- platform_game_counts: dict mapping platform fs_slug to game count
|
||||
- supported_platforms: list of all supported platforms with metadata
|
||||
"""
|
||||
|
||||
@@ -190,12 +191,47 @@ async def get_setup_library_info():
|
||||
except Exception:
|
||||
existing_platforms = []
|
||||
|
||||
# Count games for each existing platform
|
||||
platform_game_counts = {}
|
||||
if detected_structure and existing_platforms:
|
||||
cnfg = cm.get_config()
|
||||
for fs_slug in existing_platforms:
|
||||
try:
|
||||
# Determine the roms directory based on structure
|
||||
if detected_structure == "A":
|
||||
roms_path = os.path.join(
|
||||
LIBRARY_BASE_PATH, cnfg.ROMS_FOLDER_NAME, fs_slug
|
||||
)
|
||||
else: # Structure B
|
||||
roms_path = os.path.join(
|
||||
LIBRARY_BASE_PATH, fs_slug, cnfg.ROMS_FOLDER_NAME
|
||||
)
|
||||
|
||||
# Count files and folders in the roms directory
|
||||
if os.path.exists(roms_path):
|
||||
items = os.listdir(roms_path)
|
||||
# Filter out hidden files and system files
|
||||
game_count = len(
|
||||
[
|
||||
item
|
||||
for item in items
|
||||
if not item.startswith(".")
|
||||
and item not in ["_resources", "_cache"]
|
||||
]
|
||||
)
|
||||
platform_game_counts[fs_slug] = game_count
|
||||
else:
|
||||
platform_game_counts[fs_slug] = 0
|
||||
except Exception:
|
||||
platform_game_counts[fs_slug] = 0
|
||||
|
||||
# Get all supported platforms with metadata
|
||||
supported_platforms = get_supported_platforms()
|
||||
|
||||
return {
|
||||
"detected_structure": detected_structure,
|
||||
"existing_platforms": existing_platforms,
|
||||
"platform_game_counts": platform_game_counts,
|
||||
"supported_platforms": supported_platforms,
|
||||
}
|
||||
|
||||
|
||||
@@ -86,9 +86,21 @@ class FSPlatformsHandler(FSHandler):
|
||||
Returns:
|
||||
List of platform slugs.
|
||||
"""
|
||||
cnfg = cm.get_config()
|
||||
|
||||
try:
|
||||
platforms = await self.list_directories(path=self.get_platforms_directory())
|
||||
except FileNotFoundError as e:
|
||||
raise FolderStructureNotMatchException() from e
|
||||
|
||||
# For Structure B, only include directories that have a roms subfolder
|
||||
if not os.path.exists(cnfg.HIGH_PRIO_STRUCTURE_PATH):
|
||||
platforms = [
|
||||
platform
|
||||
for platform in platforms
|
||||
if os.path.exists(
|
||||
os.path.join(LIBRARY_BASE_PATH, platform, cnfg.ROMS_FOLDER_NAME)
|
||||
)
|
||||
]
|
||||
|
||||
return self._exclude_platforms(platforms)
|
||||
|
||||
@@ -9,6 +9,9 @@ const props = defineProps<{
|
||||
showCheckboxes?: boolean;
|
||||
keyPrefix?: string;
|
||||
baseIndex?: number;
|
||||
onToggleGroup?: (platforms: Platform[], checked: boolean) => void;
|
||||
isGroupFullySelected?: (platforms: Platform[]) => boolean;
|
||||
platformGameCounts?: Record<string, number>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -42,18 +45,33 @@ const countSelectedInGroup = (platforms: Platform[]) => {
|
||||
class="bg-transparent"
|
||||
>
|
||||
<v-expansion-panel-title class="text-white text-shadow">
|
||||
<strong>{{ groupName }}</strong>
|
||||
<span class="ml-2 text-caption text-grey"
|
||||
>({{ platforms.length }})</span
|
||||
>
|
||||
<v-chip
|
||||
v-if="showCheckboxes && countSelectedInGroup(platforms) > 0"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ countSelectedInGroup(platforms) }} selected
|
||||
</v-chip>
|
||||
<template #default>
|
||||
<div class="d-flex align-center w-100">
|
||||
<v-checkbox
|
||||
v-if="showCheckboxes && onToggleGroup && isGroupFullySelected"
|
||||
:model-value="isGroupFullySelected(platforms)"
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mr-2 flex-grow-0"
|
||||
@click.stop
|
||||
@update:model-value="onToggleGroup(platforms, $event as boolean)"
|
||||
/>
|
||||
<div class="flex-grow-1">
|
||||
<strong>{{ groupName }}</strong>
|
||||
<span class="ml-2 text-caption text-grey"
|
||||
>({{ platforms.length }})</span
|
||||
>
|
||||
<v-chip
|
||||
v-if="showCheckboxes && countSelectedInGroup(platforms) > 0"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ countSelectedInGroup(platforms) }} selected
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list lines="two" class="py-1 px-0 bg-transparent">
|
||||
@@ -61,7 +79,6 @@ const countSelectedInGroup = (platforms: Platform[]) => {
|
||||
v-for="platform in platforms"
|
||||
:key="platform.fs_slug"
|
||||
:platform="platform"
|
||||
:show-rom-count="false"
|
||||
>
|
||||
<template v-if="showCheckboxes" #prepend>
|
||||
<v-checkbox
|
||||
|
||||
@@ -6,6 +6,7 @@ export type LibraryStructure = "A" | "B" | null;
|
||||
export interface SetupLibraryInfo {
|
||||
detected_structure: LibraryStructure;
|
||||
existing_platforms: string[];
|
||||
platform_game_counts: Record<string, number>;
|
||||
supported_platforms: Platform[];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDisplay } from "vuetify";
|
||||
import PlatformGroupList from "@/components/Setup/PlatformGroupList.vue";
|
||||
import RDialog from "@/components/common/RDialog.vue";
|
||||
import router from "@/plugins/router";
|
||||
import { ROUTES } from "@/plugins/router";
|
||||
import { refetchCSRFToken } from "@/services/api";
|
||||
@@ -31,6 +32,9 @@ const selectedPlatforms = ref<string[]>([]);
|
||||
const creatingPlatforms = ref(false);
|
||||
const openPanels = ref<number[]>([]);
|
||||
const mobileTab = ref(0); // 0: Detected, 1: Available
|
||||
const showConfirmDialog = ref(false);
|
||||
const confirmDialogMessage = ref("");
|
||||
const confirmDialogAction = ref<(() => void) | null>(null);
|
||||
|
||||
// Use a computed property to reactively update metadataOptions based on heartbeat
|
||||
const metadataOptions = computed(() => [
|
||||
@@ -146,15 +150,21 @@ const groupPlatformsByManufacturer = (platforms: Platform[]) => {
|
||||
const groupedExistingPlatforms = computed(() => {
|
||||
if (!libraryInfo.value) return [];
|
||||
|
||||
// Only show existing platforms if a structure was actually detected
|
||||
if (!libraryInfo.value.detected_structure) return [];
|
||||
|
||||
// Get supported platform slugs for quick lookup
|
||||
const supportedSlugs = new Set(
|
||||
libraryInfo.value.supported_platforms.map((p) => p.fs_slug),
|
||||
);
|
||||
|
||||
// Get identified platforms (existing and in supported list)
|
||||
const identified = libraryInfo.value.supported_platforms.filter((p) =>
|
||||
libraryInfo.value?.existing_platforms.includes(p.fs_slug),
|
||||
);
|
||||
const identified = libraryInfo.value.supported_platforms
|
||||
.filter((p) => libraryInfo.value?.existing_platforms.includes(p.fs_slug))
|
||||
.map((p) => ({
|
||||
...p,
|
||||
rom_count: libraryInfo.value?.platform_game_counts?.[p.fs_slug] || 0,
|
||||
}));
|
||||
|
||||
// Get unidentified platforms (existing but not in supported list)
|
||||
// Create Platform objects for them with family_name="Other"
|
||||
@@ -168,6 +178,7 @@ const groupedExistingPlatforms = computed(() => {
|
||||
name: slug,
|
||||
family_name: "Other",
|
||||
generation: 999,
|
||||
rom_count: libraryInfo.value?.platform_game_counts?.[slug] || 0,
|
||||
}) as Platform,
|
||||
);
|
||||
|
||||
@@ -178,14 +189,19 @@ const groupedExistingPlatforms = computed(() => {
|
||||
// Group available platforms (not existing)
|
||||
const groupedAvailablePlatforms = computed(() => {
|
||||
if (!libraryInfo.value) return [];
|
||||
const available = libraryInfo.value.supported_platforms.filter(
|
||||
(p) => !libraryInfo.value?.existing_platforms.includes(p.fs_slug),
|
||||
);
|
||||
const available = libraryInfo.value.supported_platforms
|
||||
.filter((p) => !libraryInfo.value?.existing_platforms.includes(p.fs_slug))
|
||||
.map((p) => ({
|
||||
...p,
|
||||
rom_count: libraryInfo.value?.platform_game_counts?.[p.fs_slug] || 0,
|
||||
}));
|
||||
return groupPlatformsByManufacturer(available);
|
||||
});
|
||||
|
||||
// Check if there are existing platforms
|
||||
const hasExistingPlatforms = computed(() => {
|
||||
// Only consider platforms as existing if a structure was detected
|
||||
if (!libraryInfo.value?.detected_structure) return false;
|
||||
return (libraryInfo.value?.existing_platforms.length ?? 0) > 0;
|
||||
});
|
||||
|
||||
@@ -225,6 +241,98 @@ watch(selectAll, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Function to toggle all platforms in a group
|
||||
function toggleGroupSelection(platforms: Platform[], checked: boolean) {
|
||||
const slugs = platforms.map((p) => p.fs_slug);
|
||||
if (checked) {
|
||||
// Add all platforms from this group
|
||||
const newSlugs = slugs.filter(
|
||||
(slug) => !selectedPlatforms.value.includes(slug),
|
||||
);
|
||||
selectedPlatforms.value = [...selectedPlatforms.value, ...newSlugs];
|
||||
} else {
|
||||
// Remove all platforms from this group
|
||||
selectedPlatforms.value = selectedPlatforms.value.filter(
|
||||
(slug) => !slugs.includes(slug),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all platforms in a group are selected
|
||||
function isGroupFullySelected(platforms: Platform[]) {
|
||||
return platforms.every((p) => selectedPlatforms.value.includes(p.fs_slug));
|
||||
}
|
||||
|
||||
// Compute the count of selected available platforms (excluding existing ones)
|
||||
const selectedAvailableCount = computed(() => {
|
||||
const existingPlatforms = libraryInfo.value?.existing_platforms || [];
|
||||
return selectedPlatforms.value.filter(
|
||||
(slug) => !existingPlatforms.includes(slug),
|
||||
).length;
|
||||
});
|
||||
|
||||
// Compute the total game count for detected platforms
|
||||
const totalDetectedGames = computed(() => {
|
||||
if (!libraryInfo.value?.platform_game_counts) return 0;
|
||||
return Object.values(libraryInfo.value.platform_game_counts).reduce(
|
||||
(total, count) => total + count,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
// Function to handle next button with confirmation
|
||||
function handleNext(nextCallback: () => void) {
|
||||
if (step.value !== 1) {
|
||||
nextCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasStructure = libraryInfo.value?.detected_structure;
|
||||
const platformsToCreate = selectedPlatforms.value.filter(
|
||||
(slug) => !isPlatformExisting(slug),
|
||||
);
|
||||
|
||||
// Case 1: No structure detected and user is creating platforms
|
||||
if (!hasStructure && platformsToCreate.length > 0) {
|
||||
confirmDialogMessage.value = `No folder structure detected. RomM will create Structure A (roms/{platform}) with ${
|
||||
platformsToCreate.length
|
||||
} platform${platformsToCreate.length > 1 ? "s" : ""}. Continue?`;
|
||||
confirmDialogAction.value = nextCallback;
|
||||
showConfirmDialog.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: No structure detected and user is not selecting anything
|
||||
if (!hasStructure && platformsToCreate.length === 0) {
|
||||
confirmDialogMessage.value =
|
||||
"No folder structure detected and no platforms selected. You will need to create the folder structure manually. Continue?";
|
||||
confirmDialogAction.value = nextCallback;
|
||||
showConfirmDialog.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: Structure is detected and user selected at least one platform to create
|
||||
if (hasStructure && platformsToCreate.length > 0) {
|
||||
confirmDialogMessage.value = `RomM will create Structure A (roms/{platform}) with ${
|
||||
platformsToCreate.length
|
||||
} platform${platformsToCreate.length > 1 ? "s" : ""}. Continue?`;
|
||||
confirmDialogAction.value = nextCallback;
|
||||
showConfirmDialog.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, proceed normally
|
||||
nextCallback();
|
||||
}
|
||||
|
||||
function handleConfirmDialog() {
|
||||
showConfirmDialog.value = false;
|
||||
if (confirmDialogAction.value) {
|
||||
confirmDialogAction.value();
|
||||
confirmDialogAction.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLibraryInfo() {
|
||||
loadingLibraryInfo.value = true;
|
||||
try {
|
||||
@@ -358,7 +466,7 @@ onMounted(() => {
|
||||
|
||||
<v-stepper-window
|
||||
class="flex-grow-1 mb-4"
|
||||
:class="{ 'align-content-center': step != 1 }"
|
||||
:class="{ 'align-content-center': step != 1 || loadingLibraryInfo }"
|
||||
>
|
||||
<v-stepper-window-item :key="1" :value="1" class="h-100">
|
||||
<v-row no-gutters class="h-100">
|
||||
@@ -411,23 +519,41 @@ onMounted(() => {
|
||||
<!-- Desktop: Two columns side by side -->
|
||||
<template v-if="!xs">
|
||||
<!-- Existing platforms column -->
|
||||
<v-col v-if="hasExistingPlatforms" cols="12" md="6">
|
||||
<v-col
|
||||
v-if="hasExistingPlatforms"
|
||||
cols="12"
|
||||
md="6"
|
||||
class="pr-2"
|
||||
>
|
||||
<div class="text-white text-center text-shadow mb-2">
|
||||
<strong>Detected Platforms</strong>
|
||||
</div>
|
||||
<div
|
||||
class="overflow-y-auto pr-4"
|
||||
style="max-height: 500px"
|
||||
>
|
||||
<div class="mb-2 ml-4">
|
||||
<v-chip label>
|
||||
{{ libraryInfo?.existing_platforms.length }} platforms
|
||||
</v-chip>
|
||||
<v-chip class="ml-2" variant="tonal" label>
|
||||
{{ totalDetectedGames }} game{{
|
||||
totalDetectedGames !== 1 ? "s" : ""
|
||||
}}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="overflow-y-auto" style="max-height: 500px">
|
||||
<PlatformGroupList
|
||||
:grouped-platforms="groupedExistingPlatforms"
|
||||
key-prefix="existing"
|
||||
:platform-game-counts="
|
||||
libraryInfo?.platform_game_counts
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Available platforms to create column -->
|
||||
<v-col cols="12" :md="hasExistingPlatforms ? 6 : 12">
|
||||
<v-col
|
||||
cols="12"
|
||||
:md="hasExistingPlatforms ? 6 : 12"
|
||||
class="pl-2"
|
||||
>
|
||||
<div class="text-white text-center text-shadow mb-2">
|
||||
<strong>{{
|
||||
hasExistingPlatforms
|
||||
@@ -435,16 +561,28 @@ onMounted(() => {
|
||||
: "Select Platforms to Create"
|
||||
}}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="overflow-y-auto pr-4"
|
||||
style="max-height: 500px"
|
||||
>
|
||||
<div class="mb-2 ml-4">
|
||||
<v-chip
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="selectAll = !selectAll"
|
||||
label
|
||||
>
|
||||
{{ selectAll ? "Deselect All" : "Select All" }}
|
||||
</v-chip>
|
||||
<v-chip class="ml-2" label
|
||||
>{{ selectedAvailableCount }} selected</v-chip
|
||||
>
|
||||
</div>
|
||||
<div class="overflow-y-auto" style="max-height: 500px">
|
||||
<PlatformGroupList
|
||||
:grouped-platforms="groupedAvailablePlatforms"
|
||||
v-model:selected-platforms="selectedPlatforms"
|
||||
:show-checkboxes="true"
|
||||
key-prefix="available"
|
||||
:base-index="groupedExistingPlatforms.length"
|
||||
:on-toggle-group="toggleGroupSelection"
|
||||
:is-group-fully-selected="isGroupFullySelected"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -471,29 +609,52 @@ onMounted(() => {
|
||||
<v-window v-model="mobileTab">
|
||||
<!-- Detected platforms tab -->
|
||||
<v-window-item v-if="hasExistingPlatforms" :value="0">
|
||||
<div
|
||||
class="overflow-y-auto pr-4"
|
||||
style="max-height: 400px"
|
||||
>
|
||||
<div class="mb-2 ml-4">
|
||||
<v-chip label>
|
||||
{{ libraryInfo?.existing_platforms.length }}
|
||||
platforms
|
||||
</v-chip>
|
||||
<v-chip class="ml-2" variant="tonal" label>
|
||||
{{ totalDetectedGames }} game{{
|
||||
totalDetectedGames !== 1 ? "s" : ""
|
||||
}}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="overflow-y-auto" style="max-height: 400px">
|
||||
<PlatformGroupList
|
||||
:grouped-platforms="groupedExistingPlatforms"
|
||||
key-prefix="existing-mobile"
|
||||
:platform-game-counts="
|
||||
libraryInfo?.platform_game_counts
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Available platforms tab -->
|
||||
<v-window-item :value="hasExistingPlatforms ? 1 : 0">
|
||||
<div
|
||||
class="overflow-y-auto pr-4"
|
||||
style="max-height: 400px"
|
||||
>
|
||||
<div class="mb-2 ml-4">
|
||||
<v-chip
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="selectAll = !selectAll"
|
||||
label
|
||||
>
|
||||
{{ selectAll ? "Deselect All" : "Select All" }}
|
||||
</v-chip>
|
||||
<v-chip class="ml-2" label
|
||||
>{{ selectedAvailableCount }} selected</v-chip
|
||||
>
|
||||
</div>
|
||||
<div class="overflow-y-auto" style="max-height: 500px">
|
||||
<PlatformGroupList
|
||||
:grouped-platforms="groupedAvailablePlatforms"
|
||||
v-model:selected-platforms="selectedPlatforms"
|
||||
:show-checkboxes="true"
|
||||
key-prefix="available-mobile"
|
||||
:base-index="groupedExistingPlatforms.length"
|
||||
:on-toggle-group="toggleGroupSelection"
|
||||
:is-group-fully-selected="isGroupFullySelected"
|
||||
/>
|
||||
</div>
|
||||
</v-window-item>
|
||||
@@ -626,8 +787,8 @@ onMounted(() => {
|
||||
<v-btn
|
||||
class="text-white text-shadow"
|
||||
:loading="isLastStep && creatingPlatforms"
|
||||
@click="!isLastStep ? next() : finishWizard()"
|
||||
@keydown.enter="!isLastStep ? next() : finishWizard()"
|
||||
@click="!isLastStep ? handleNext(next) : finishWizard()"
|
||||
@keydown.enter="!isLastStep ? handleNext(next) : finishWizard()"
|
||||
>
|
||||
{{ !isLastStep ? "Next" : "Finish" }}
|
||||
</v-btn>
|
||||
@@ -636,5 +797,42 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
</v-stepper>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<RDialog
|
||||
v-model="showConfirmDialog"
|
||||
icon="mdi-alert"
|
||||
width="500"
|
||||
@close="showConfirmDialog = false"
|
||||
>
|
||||
<template #header>
|
||||
<v-row class="ml-2">Confirm Action</v-row>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-body-1 pa-4">
|
||||
{{ confirmDialogMessage }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<v-row class="justify-center my-2" no-gutters>
|
||||
<v-btn-group divided density="compact">
|
||||
<v-btn class="bg-toplayer" @click="showConfirmDialog = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="bg-toplayer text-primary"
|
||||
@click="handleConfirmDialog"
|
||||
>
|
||||
Continue
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</v-row>
|
||||
</template>
|
||||
</RDialog>
|
||||
</v-card>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.v-expansion-panel-text__wrapper {
|
||||
padding: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user