massive refactor of data loading in console mode

This commit is contained in:
Georges-Antoine Assi
2025-10-21 10:42:59 -04:00
parent 94283ebd61
commit 6bde10fe97
16 changed files with 352 additions and 224 deletions

View File

@@ -207,7 +207,6 @@ class RomSchema(BaseModel):
platform_id: int
platform_slug: str
platform_fs_slug: str
platform_name: str
platform_custom_name: str | None
platform_display_name: str

View File

@@ -263,10 +263,6 @@ class Rom(BaseModel):
def platform_fs_slug(self) -> str:
return self.platform.fs_slug
@property
def platform_name(self) -> str:
return self.platform.name
@property
def platform_custom_name(self) -> str | None:
return self.platform.custom_name

View File

@@ -34,7 +34,6 @@ export type DetailedRomSchema = {
platform_id: number;
platform_slug: string;
platform_fs_slug: string;
platform_name: string;
platform_custom_name: (string | null);
platform_display_name: string;
fs_name: string;

View File

@@ -28,7 +28,6 @@ export type SimpleRomSchema = {
platform_id: number;
platform_slug: string;
platform_fs_slug: string;
platform_name: string;
platform_custom_name: (string | null);
platform_display_name: string;
fs_name: string;

View File

@@ -20,7 +20,7 @@ const releaseDate = new Date(
});
const platformsStore = storePlatforms();
const { filteredPlatforms } = storeToRefs(platformsStore);
const { allPlatforms } = storeToRefs(platformsStore);
const hashMatches = computed(() => {
return [
@@ -81,7 +81,7 @@ const hashMatches = computed(() => {
>
<MissingFromFSIcon
v-if="
filteredPlatforms.find((p) => p.id === rom.platform_id)
allPlatforms.find((p) => p.id === rom.platform_id)
?.missing_from_fs
"
class="mr-2"
@@ -90,7 +90,7 @@ const hashMatches = computed(() => {
<PlatformIcon
:key="rom.platform_slug"
:slug="rom.platform_slug"
:name="rom.platform_name"
:name="rom.platform_display_name"
:fs-slug="rom.platform_fs_slug"
:size="30"
class="mr-2"

View File

@@ -72,7 +72,7 @@ const {
filterLanguages,
} = storeToRefs(galleryFilterStore);
const { filteredRoms } = storeToRefs(romsStore);
const { filteredPlatforms } = storeToRefs(platformsStore);
const { allPlatforms } = storeToRefs(platformsStore);
const emitter = inject<Emitter<Events>>("emitter");
const onFilterChange = debounce(
@@ -305,7 +305,7 @@ onMounted(async () => {
);
watch(
() => filteredPlatforms.value,
() => allPlatforms.value,
async () => setFilters(),
{ immediate: true }, // Ensure watcher is triggered immediately
);

View File

@@ -36,10 +36,8 @@ const editable = ref(false);
/>
</template>
<p>
Versions of the same platform. A common example is Capcom Play System
1 is an arcade system. Platform versions will let you setup a custom
platform for RomM to import and tell RomM which platform it needs to
scrape against.
Platform versions allow you to create custom platform entries for
games that belong to the same system but have different versions.
</p>
</v-tooltip>
</template>

View File

@@ -12,11 +12,11 @@ const props = defineProps<{
}>();
const { t } = useI18n();
const platformsStore = storePlatforms();
const { filteredPlatforms } = storeToRefs(platformsStore);
const { allPlatforms } = storeToRefs(platformsStore);
const orderBy = ref<"name" | "size" | "count">("name");
const sortedPlatforms = computed(() => {
const platforms = [...filteredPlatforms.value];
const platforms = [...allPlatforms.value];
if (orderBy.value === "size") {
return platforms.sort(
(a, b) => Number(b.fs_size_bytes) - Number(a.fs_size_bytes),

View File

@@ -311,7 +311,7 @@ onBeforeUnmount(() => {
:key="rom.platform_slug"
:size="25"
:slug="rom.platform_slug"
:name="rom.platform_name"
:name="rom.platform_display_name"
:fs-slug="rom.platform_fs_slug"
class="ml-1"
/>

View File

@@ -87,7 +87,7 @@ socket.on("scan:scanning_rom", (rom: SimpleRom) => {
// Add the platform if the socket dropped and it's missing
if (!scannedPlatform) {
scanningPlatforms.value.push({
name: rom.platform_name,
name: rom.platform_display_name,
slug: rom.platform_slug,
id: rom.platform_id,
fs_slug: rom.platform_fs_slug,

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useIdle } from "@vueuse/core";
import { onMounted, onUnmounted, provide } from "vue";
import { useIdle, useLocalStorage } from "@vueuse/core";
import { onBeforeMount, onMounted, onUnmounted, provide } from "vue";
import { type RouteLocationNormalized } from "vue-router";
import { useRouter } from "vue-router";
import { useConsoleTheme } from "@/console/composables/useConsoleTheme";
@@ -8,12 +8,28 @@ import { InputBus, InputBusSymbol } from "@/console/input/bus";
import { attachGamepad } from "@/console/input/gamepad";
import { attachKeyboard } from "@/console/input/keyboard";
import { ROUTES } from "@/plugins/router";
import storeCollections from "@/stores/collections";
import storeNavigation from "@/stores/navigation";
import storePlatforms from "@/stores/platforms";
const router = useRouter();
const bus = new InputBus();
const themeStore = useConsoleTheme();
provide(InputBusSymbol, bus);
const navigationStore = storeNavigation();
const platformsStore = storePlatforms();
const collectionsStore = storeCollections();
const showVirtualCollections = useLocalStorage(
"settings.showVirtualCollections",
true,
);
const virtualCollectionTypeRef = useLocalStorage(
"settings.virtualCollectionType",
"collection",
);
// Define route hierarchy for transition direction logic
const routeHierarchy = {
[ROUTES.CONSOLE_HOME]: 0,
@@ -53,6 +69,17 @@ const { idle: mouseIdle } = useIdle(100, {
let detachKeyboard: (() => void) | null = null;
let detachGamepad: (() => void) | null = null;
onBeforeMount(() => {
platformsStore.fetchPlatforms();
collectionsStore.fetchCollections();
collectionsStore.fetchSmartCollections();
if (showVirtualCollections) {
collectionsStore.fetchVirtualCollections(virtualCollectionTypeRef.value);
}
navigationStore.reset();
});
onMounted(() => {
themeStore.initializeTheme();

View File

@@ -3,6 +3,7 @@ import { useRouter } from "vue-router";
const props = defineProps<{
text?: string;
subtext?: string;
onBack?: () => void;
}>();
@@ -25,11 +26,19 @@ function goBack() {
>
<span aria-hidden="true" class="text-lg leading-none"></span>
</button>
<span
v-if="text"
class="ml-3 text-white/90 text-3xl font-bold drop-shadow select-none"
>
{{ text }}
</span>
<div class="ml-3">
<span
v-if="text"
class="text-white/90 text-3xl font-bold drop-shadow select-none"
>
{{ text }}
</span>
<span
v-if="subtext"
class="text-white/50 text-sm font-normal drop-shadow select-none"
>
{{ subtext }}
</span>
</div>
</div>
</template>

View File

@@ -574,7 +574,7 @@ onUnmounted(() => {
}"
>
{{
rom.platform_name ||
rom.platform_display_name ||
(rom.platform_slug || "RETRO")?.toString().toUpperCase()
}}
</span>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import {
computed,
onMounted,
@@ -6,11 +7,9 @@ import {
ref,
nextTick,
useTemplateRef,
watch,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import type { CollectionSchema } from "@/__generated__/models/CollectionSchema";
import type { SmartCollectionSchema } from "@/__generated__/models/SmartCollectionSchema";
import type { VirtualCollectionSchema } from "@/__generated__/models/VirtualCollectionSchema";
import useFavoriteToggle from "@/composables/useFavoriteToggle";
import BackButton from "@/console/components/BackButton.vue";
import GameCard from "@/console/components/GameCard.vue";
@@ -22,8 +21,8 @@ import { useRovingDom } from "@/console/composables/useRovingDom";
import { useSpatialNav } from "@/console/composables/useSpatialNav";
import type { InputAction } from "@/console/input/actions";
import { ROUTES } from "@/plugins/router";
import collectionApi from "@/services/api/collection";
import storeCollections from "@/stores/collections";
import storeConfig from "@/stores/config";
import storeConsole from "@/stores/console";
import storeGalleryFilter from "@/stores/galleryFilter";
import storePlatforms from "@/stores/platforms";
@@ -32,61 +31,45 @@ import storeRoms, { type SimpleRom } from "@/stores/roms";
const route = useRoute();
const router = useRouter();
const consoleStore = storeConsole();
const platformsStore = storePlatforms();
const collectionsStore = storeCollections();
const galleryFilterStore = storeGalleryFilter();
const platformsStore = storePlatforms();
const { allPlatforms } = storeToRefs(platformsStore);
const collectionsStore = storeCollections();
const { allCollections, smartCollections, virtualCollections } =
storeToRefs(collectionsStore);
const romsStore = storeRoms();
const {
filteredRoms,
allRoms,
fetchingRoms,
currentPlatform,
currentCollection,
currentSmartCollection,
currentVirtualCollection,
} = storeToRefs(romsStore);
const configStore = storeConfig();
const { config } = storeToRefs(configStore);
const { toggleFavorite: toggleFavoriteComposable } = useFavoriteToggle();
const { setSelectedBackgroundArt, clearSelectedBackgroundArt } =
useBackgroundArt();
const isPlatformRoute = route.name === ROUTES.CONSOLE_PLATFORM;
const isCollectionRoute = route.name === ROUTES.CONSOLE_COLLECTION;
const isSmartCollectionRoute = route.name === ROUTES.CONSOLE_SMART_COLLECTION;
const isVirtualCollectionRoute =
route.name === ROUTES.CONSOLE_VIRTUAL_COLLECTION;
const platformId =
isCollectionRoute || isSmartCollectionRoute || isVirtualCollectionRoute
? null
: Number(route.params.id);
const collectionId = isCollectionRoute ? Number(route.params.id) : null;
const smartCollectionId = isSmartCollectionRoute
? Number(route.params.id)
: null;
const virtualCollectionId = isVirtualCollectionRoute
? String(route.params.id)
: null;
const roms = ref<SimpleRom[]>([]);
const collection = ref<CollectionSchema | null>(null);
const smartCollection = ref<SmartCollectionSchema | null>(null);
const virtualCollection = ref<VirtualCollectionSchema | null>(null);
const loading = ref(true);
const error = ref("");
const selectedIndex = ref(0);
const loadedMap = ref<Record<number, boolean>>({});
const inAlphabet = ref(false);
const alphaIndex = ref(0);
const gridRef = useTemplateRef<HTMLDivElement>("game-grid-ref");
// Initialize selection from store
if (platformId != null) {
selectedIndex.value = consoleStore.getPlatformGameIndex(platformId);
} else if (collectionId != null) {
selectedIndex.value = consoleStore.getCollectionGameIndex(collectionId);
} else if (smartCollectionId != null) {
selectedIndex.value = consoleStore.getCollectionGameIndex(smartCollectionId);
} else if (virtualCollectionId != null) {
selectedIndex.value = consoleStore.getCollectionGameIndex(
Number(virtualCollectionId),
);
}
// Generate alphabet letters dynamically based on available games
const letters = computed(() => {
const letterSet = new Set<string>();
roms.value.forEach(({ name }) => {
filteredRoms.value.forEach(({ name }) => {
if (!name) return;
const normalized = normalizeTitle(name);
@@ -111,15 +94,24 @@ const letters = computed(() => {
});
function persistIndex() {
if (platformId != null) {
consoleStore.setPlatformGameIndex(platformId, selectedIndex.value);
} else if (collectionId != null) {
consoleStore.setCollectionGameIndex(collectionId, selectedIndex.value);
} else if (smartCollectionId != null) {
consoleStore.setCollectionGameIndex(smartCollectionId, selectedIndex.value);
} else if (virtualCollectionId != null) {
if (currentPlatform.value != null) {
consoleStore.setPlatformGameIndex(
currentPlatform.value.id,
selectedIndex.value,
);
} else if (currentCollection.value != null) {
consoleStore.setCollectionGameIndex(
Number(virtualCollectionId),
currentCollection.value.id,
selectedIndex.value,
);
} else if (currentSmartCollection.value != null) {
consoleStore.setSmartCollectionGameIndex(
currentSmartCollection.value.id,
selectedIndex.value,
);
} else if (currentVirtualCollection.value != null) {
consoleStore.setVirtualCollectionGameIndex(
currentVirtualCollection.value.id,
selectedIndex.value,
);
}
@@ -132,25 +124,34 @@ function navigateBack() {
const headerTitle = computed(() => {
if (isCollectionRoute) {
return collection.value?.name || "Collection";
return currentCollection.value?.name || "Collection";
}
if (isSmartCollectionRoute) {
return smartCollection.value?.name || "Smart Collection";
return currentSmartCollection.value?.name || "Smart Collection";
}
if (isVirtualCollectionRoute) {
return virtualCollection.value?.name || "Virtual Collection";
return currentVirtualCollection.value?.name || "Virtual Collection";
}
return (
current.value?.platform_name ||
current.value?.platform_slug?.toUpperCase() ||
"Platform"
currentPlatform.value?.display_name ||
currentPlatform.value?.slug.toUpperCase()
);
});
const current = computed(
() => roms.value[selectedIndex.value] || roms.value[0],
);
const headerSubtext = computed(() => {
if (isCollectionRoute) {
return currentCollection.value?.description || "";
}
if (isSmartCollectionRoute) {
return currentSmartCollection.value?.description || "";
}
if (isVirtualCollectionRoute) {
return currentVirtualCollection.value?.description || "";
}
if (!currentPlatform.value?.fs_slug) return "";
return config.value.PLATFORMS_VERSIONS[currentPlatform.value.fs_slug];
});
function getCols(): number {
if (!gridRef.value) return 4;
@@ -176,10 +177,10 @@ const {
moveRight,
moveUp,
moveDown: moveDownBasic,
} = useSpatialNav(selectedIndex, getCols, () => roms.value.length);
} = useSpatialNav(selectedIndex, getCols, () => filteredRoms.value.length);
function handleAction(action: InputAction): boolean {
if (!roms.value.length) return false;
if (!filteredRoms.value.length) return false;
if (inAlphabet.value) {
if (action === "moveLeft") {
inAlphabet.value = false;
@@ -198,7 +199,7 @@ function handleAction(action: InputAction): boolean {
}
if (action === "confirm") {
const L = Array.from(letters.value)[alphaIndex.value];
const idx = roms.value.findIndex((r) => {
const idx = filteredRoms.value.findIndex((r) => {
const normalized = normalizeTitle(r.name || "");
if (L === "#") {
return /^[0-9]/.test(normalized);
@@ -245,7 +246,7 @@ function handleAction(action: InputAction): boolean {
moveDownBasic();
if (selectedIndex.value === before) {
const cols = getCols();
const count = roms.value.length;
const count = filteredRoms.value.length;
const totalRows = Math.ceil(count / cols);
const currentRow = Math.floor(before / cols);
if (totalRows > currentRow + 1) {
@@ -258,11 +259,14 @@ function handleAction(action: InputAction): boolean {
navigateBack();
return true;
case "confirm": {
selectAndOpen(selectedIndex.value, roms.value[selectedIndex.value]);
selectAndOpen(
selectedIndex.value,
filteredRoms.value[selectedIndex.value],
);
return true;
}
case "toggleFavorite": {
const rom = roms.value[selectedIndex.value];
const rom = filteredRoms.value[selectedIndex.value];
if (rom) toggleFavoriteComposable(rom);
return true;
}
@@ -283,10 +287,14 @@ function selectAndOpen(i: number, rom: SimpleRom) {
persistIndex();
const query: Record<string, number | string> = {};
if (platformId != null) query.id = platformId;
if (isCollectionRoute) query.collection = collectionId!;
if (isSmartCollectionRoute) query.smartCollection = smartCollectionId!;
if (isVirtualCollectionRoute) query.virtualCollection = virtualCollectionId!;
if (isPlatformRoute && currentPlatform.value != null)
query.id = currentPlatform.value.id;
if (isCollectionRoute && currentCollection.value != null)
query.collection = currentCollection.value.id;
if (isSmartCollectionRoute && currentSmartCollection.value != null)
query.smartCollection = currentSmartCollection.value.id;
if (isVirtualCollectionRoute && currentVirtualCollection.value != null)
query.virtualCollection = currentVirtualCollection.value.id;
router.push({
name: ROUTES.CONSOLE_ROM,
@@ -296,7 +304,7 @@ function selectAndOpen(i: number, rom: SimpleRom) {
}
function jumpToLetter(L: string) {
const idx = roms.value.findIndex((r) => {
const idx = filteredRoms.value.findIndex((r) => {
const normalized = normalizeTitle(r.name || "");
if (L === "#") {
return /^[0-9]/.test(normalized);
@@ -316,68 +324,171 @@ function normalizeTitle(name: string) {
let off: (() => void) | null = null;
onMounted(async () => {
try {
if (platformId != null) {
const currentPlatform = platformsStore.get(platformId);
if (currentPlatform) romsStore.setCurrentPlatform(currentPlatform);
} else if (collectionId != null) {
const currentCollection = collectionsStore.getCollection(collectionId);
if (currentCollection) romsStore.setCurrentCollection(currentCollection);
} else if (smartCollectionId != null) {
const currentSmartCollection =
collectionsStore.getSmartCollection(smartCollectionId);
if (currentSmartCollection)
romsStore.setCurrentSmartCollection(currentSmartCollection);
} else if (virtualCollectionId != null) {
const currentVirtualCollection =
collectionsStore.getVirtualCollection(virtualCollectionId);
if (currentVirtualCollection)
romsStore.setCurrentVirtualCollection(currentVirtualCollection);
}
function resetGallery() {
romsStore.reset();
galleryFilterStore.resetFilters();
galleryFilterStore.activeFilterDrawer = false;
}
romsStore.setLimit(500);
romsStore.setOrderBy("name");
romsStore.setOrderDir("asc");
romsStore.resetPagination();
async function fetchRoms() {
romsStore.setLimit(500);
romsStore.setOrderBy("name");
romsStore.setOrderDir("asc");
romsStore.resetPagination();
const fetchedRoms = await romsStore.fetchRoms({
galleryFilter: galleryFilterStore,
concat: false,
});
roms.value = fetchedRoms;
const fetchedRoms = await romsStore.fetchRoms({
galleryFilter: galleryFilterStore,
concat: false,
});
if (collectionId != null) {
const { data: col } = await collectionApi.getCollection(collectionId);
collection.value = col ?? null;
} else if (smartCollectionId != null) {
const { data: smartCol } =
await collectionApi.getSmartCollection(smartCollectionId);
smartCollection.value = smartCol ?? null;
} else if (virtualCollectionId != null) {
const { data: virtualCol } =
await collectionApi.getVirtualCollection(virtualCollectionId);
virtualCollection.value = virtualCol ?? null;
}
for (const r of roms.value) {
if (!r.url_cover && !r.path_cover_large && !r.path_cover_small) {
loadedMap.value[r.id] = true;
}
}
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "Failed to load roms";
} finally {
loading.value = false;
}
if (selectedIndex.value >= roms.value.length) selectedIndex.value = 0;
if (selectedIndex.value >= fetchedRoms.length) selectedIndex.value = 0;
await nextTick();
cardElementAt(selectedIndex.value)?.scrollIntoView({
block: "center",
inline: "nearest",
behavior: "instant" as ScrollBehavior,
});
}
onMounted(async () => {
const routePlatformId = isPlatformRoute ? Number(route.params.id) : null;
const routeCollectionId = isCollectionRoute ? Number(route.params.id) : null;
const routeSmartCollectionId = isSmartCollectionRoute
? Number(route.params.id)
: null;
const routeVirtualCollectionId = isVirtualCollectionRoute
? String(route.params.id)
: null;
watch(
() => allPlatforms.value,
async (platforms) => {
if (platforms.length > 0) {
if (platforms.some((platform) => platform.id === routePlatformId)) {
const platform = platforms.find(
(platform) => platform.id === routePlatformId,
);
// Check if the current platform is different or no ROMs have been loaded
if (
(currentPlatform.value?.id !== routePlatformId ||
allRoms.value.length === 0) &&
platform
) {
if (currentPlatform.value) resetGallery();
selectedIndex.value = consoleStore.getPlatformGameIndex(
platform.id,
);
romsStore.setCurrentPlatform(platform);
document.title = `${platform.display_name}`;
await fetchRoms();
}
}
}
},
{ immediate: true }, // Ensure watcher is triggered immediately
);
watch(
() => allCollections.value,
async (collections) => {
if (collections.length > 0) {
if (
collections.some((collection) => collection.id === routeCollectionId)
) {
const collection = collections.find(
(collection) => collection.id === routeCollectionId,
);
// Check if the current collection is different or no ROMs have been loaded
if (
(currentCollection.value?.id !== routeCollectionId ||
allRoms.value.length === 0) &&
collection
) {
if (currentCollection.value) resetGallery();
selectedIndex.value = consoleStore.getCollectionGameIndex(
collection.id,
);
romsStore.setCurrentCollection(collection);
document.title = `${collection.name}`;
await fetchRoms();
}
}
}
},
{ immediate: true }, // Ensure watcher is triggered immediately
);
watch(
() => smartCollections.value,
async (smartCollections) => {
if (smartCollections.length > 0) {
if (
smartCollections.some(
(smartCollection) => smartCollection.id === routeSmartCollectionId,
)
) {
const smartCollection = smartCollections.find(
(smartCollection) => smartCollection.id === routeSmartCollectionId,
);
// Check if the current smartCollection is different or no ROMs have been loaded
if (
(currentSmartCollection.value?.id !== routeSmartCollectionId ||
allRoms.value.length === 0) &&
smartCollection
) {
if (currentSmartCollection.value) resetGallery();
selectedIndex.value = consoleStore.getSmartCollectionGameIndex(
smartCollection.id,
);
romsStore.setCurrentSmartCollection(smartCollection);
document.title = `${smartCollection.name}`;
await fetchRoms();
}
}
}
},
{ immediate: true }, // Ensure watcher is triggered immediately
);
watch(
() => virtualCollections.value,
async (virtualCollections) => {
if (virtualCollections.length > 0) {
if (
virtualCollections.some(
(virtualCollection) =>
virtualCollection.id === routeVirtualCollectionId,
)
) {
const virtualCollection = virtualCollections.find(
(virtualCollection) =>
virtualCollection.id === routeVirtualCollectionId,
);
// Check if the current virtualCollection is different or no ROMs have been loaded
if (
(currentVirtualCollection.value?.id !== routeVirtualCollectionId ||
allRoms.value.length === 0) &&
virtualCollection
) {
if (currentVirtualCollection.value) resetGallery();
selectedIndex.value = consoleStore.getVirtualCollectionGameIndex(
virtualCollection.id,
);
romsStore.setCurrentVirtualCollection(virtualCollection);
document.title = `${virtualCollection.name}`;
await fetchRoms();
}
}
}
},
{ immediate: true }, // Ensure watcher is triggered immediately
);
off = subscribe(handleAction);
});
@@ -405,27 +516,27 @@ function handleItemDeselected() {
class="relative min-h-screen overflow-y-auto overflow-x-hidden max-w-[100vw] flex"
@wheel.prevent
>
<BackButton :text="headerTitle" :on-back="navigateBack" />
<BackButton
:text="headerTitle"
:subtext="headerSubtext"
:on-back="navigateBack"
/>
<div
class="relative flex-1 min-w-0 pr-[40px]"
:style="{ width: 'calc(100vw - 40px)' }"
>
<div
v-if="loading"
v-if="fetchingRoms"
class="text-center mt-8"
:style="{ color: 'var(--console-loading-text)' }"
>
Loading games
</div>
<div
v-else-if="error"
class="text-center mt-8"
:style="{ color: 'var(--console-error-text)' }"
>
{{ error }}
</div>
<div v-else>
<div v-if="roms.length === 0" class="text-center text-fgDim p-4">
<div
v-if="filteredRoms.length === 0"
class="text-center text-fgDim p-4"
>
No games found.
</div>
<div
@@ -434,7 +545,7 @@ function handleItemDeselected() {
@wheel.prevent
>
<GameCard
v-for="(rom, i) in roms"
v-for="(rom, i) in filteredRoms"
:key="rom.id"
:rom="rom"
:index="i"

View File

@@ -9,10 +9,6 @@ import {
useTemplateRef,
} from "vue";
import { useRouter } from "vue-router";
import type { CollectionSchema } from "@/__generated__/models/CollectionSchema";
import type { PlatformSchema } from "@/__generated__/models/PlatformSchema";
import type { SmartCollectionSchema } from "@/__generated__/models/SmartCollectionSchema";
import type { VirtualCollectionSchema } from "@/__generated__/models/VirtualCollectionSchema";
import RIsotipo from "@/components/common/RIsotipo.vue";
import useFavoriteToggle from "@/composables/useFavoriteToggle";
import CollectionCard from "@/console/components/CollectionCard.vue";
@@ -31,20 +27,23 @@ import {
import { useInputScope } from "@/console/composables/useInputScope";
import { useRovingDom } from "@/console/composables/useRovingDom";
import { useSpatialNav } from "@/console/composables/useSpatialNav";
import { isSupportedPlatform } from "@/console/constants/platforms";
import type { InputAction } from "@/console/input/actions";
import { ROUTES } from "@/plugins/router";
import collectionApi from "@/services/api/collection";
import platformApi from "@/services/api/platform";
import storeCollections from "@/stores/collections";
import storeConsole from "@/stores/console";
import storePlatforms from "@/stores/platforms";
import storeRoms from "@/stores/roms";
import type { SimpleRom } from "@/stores/roms";
const router = useRouter();
const platformsStore = storePlatforms();
const { allPlatforms, fetchingPlatforms } = storeToRefs(platformsStore);
const collectionsStore = storeCollections();
const consoleStore = storeConsole();
const { allCollections, smartCollections, virtualCollections } =
storeToRefs(collectionsStore);
const romsStore = storeRoms();
const { recentRoms } = storeToRefs(romsStore);
const consoleStore = storeConsole();
const {
navigationMode,
platformIndex,
@@ -59,12 +58,6 @@ const { setSelectedBackgroundArt, clearSelectedBackgroundArt } =
useBackgroundArt();
const { subscribe } = useInputScope();
const platforms = ref<PlatformSchema[]>([]);
const recentRoms = ref<SimpleRom[]>([]);
const collections = ref<CollectionSchema[]>([]);
const smartCollections = ref<SmartCollectionSchema[]>([]);
const virtualCollections = ref<VirtualCollectionSchema[]>([]);
const loadingPlatforms = ref(true);
const errorMessage = ref("");
const showSettings = ref(false);
@@ -104,8 +97,8 @@ const virtualCollectionElementAt = (i: number) =>
// Spatial navigation
const { moveLeft: moveSystemLeft, moveRight: moveSystemRight } = useSpatialNav(
platformIndex,
() => platforms.value.length || 1,
() => platforms.value.length,
() => allPlatforms.value.length || 1,
() => allPlatforms.value.length,
);
const { moveLeft: moveRecentLeft, moveRight: moveRecentRight } = useSpatialNav(
recentIndex,
@@ -115,8 +108,8 @@ const { moveLeft: moveRecentLeft, moveRight: moveRecentRight } = useSpatialNav(
const { moveLeft: moveCollectionLeft, moveRight: moveCollectionRight } =
useSpatialNav(
collectionsIndex,
() => collections.value.length || 1,
() => collections.value.length,
() => allCollections.value.length || 1,
() => allCollections.value.length,
);
const {
moveLeft: moveSmartCollectionLeft,
@@ -219,7 +212,7 @@ const navigationFunctions = {
const before = platformIndex.value;
moveSystemLeft();
if (platformIndex.value === before) {
platformIndex.value = Math.max(0, platforms.value.length - 1);
platformIndex.value = Math.max(0, allPlatforms.value.length - 1);
}
},
next: () => {
@@ -230,10 +223,10 @@ const navigationFunctions = {
}
},
confirm: () => {
if (!platforms.value[platformIndex.value]) return false;
if (!allPlatforms.value[platformIndex.value]) return false;
router.push({
name: ROUTES.CONSOLE_PLATFORM,
params: { id: platforms.value[platformIndex.value].id },
params: { id: allPlatforms.value[platformIndex.value].id },
});
return true;
},
@@ -268,7 +261,7 @@ const navigationFunctions = {
const before = collectionsIndex.value;
moveCollectionLeft();
if (collectionsIndex.value === before) {
collectionsIndex.value = Math.max(0, collections.value.length - 1);
collectionsIndex.value = Math.max(0, allCollections.value.length - 1);
}
},
next: () => {
@@ -279,10 +272,10 @@ const navigationFunctions = {
}
},
confirm: () => {
if (!collections.value[collectionsIndex.value]) return false;
if (!allCollections.value[collectionsIndex.value]) return false;
router.push({
name: ROUTES.CONSOLE_COLLECTION,
params: { id: collections.value[collectionsIndex.value].id },
params: { id: allCollections.value[collectionsIndex.value].id },
});
return true;
},
@@ -526,7 +519,7 @@ function handleAction(action: InputAction): boolean {
}
if (currentMode === "smartCollections") {
navigationMode.value =
collections.value.length > 0
allCollections.value.length > 0
? "collections"
: recentRoms.value.length > 0
? "recent"
@@ -538,7 +531,7 @@ function handleAction(action: InputAction): boolean {
navigationMode.value =
smartCollections.value.length > 0
? "smartCollections"
: collections.value.length > 0
: allCollections.value.length > 0
? "collections"
: recentRoms.value.length > 0
? "recent"
@@ -553,7 +546,7 @@ function handleAction(action: InputAction): boolean {
navigationMode.value =
recentRoms.value.length > 0
? "recent"
: collections.value.length > 0
: allCollections.value.length > 0
? "collections"
: smartCollections.value.length > 0
? "smartCollections"
@@ -565,7 +558,7 @@ function handleAction(action: InputAction): boolean {
}
if (currentMode === "recent") {
navigationMode.value =
collections.value.length > 0
allCollections.value.length > 0
? "collections"
: smartCollections.value.length > 0
? "smartCollections"
@@ -625,41 +618,10 @@ function handleAction(action: InputAction): boolean {
}
onMounted(async () => {
try {
const [
{ data: plats },
recents,
{ data: cols },
{ data: smartCols },
{ data: virtualCols },
] = await Promise.all([
platformApi.getPlatforms(),
romsStore.fetchRecentRoms(),
collectionApi.getCollections(),
collectionApi.getSmartCollections(),
collectionApi.getVirtualCollections({ type: "collection" }),
]);
platforms.value = plats.filter(
(p) => p.rom_count > 0 && isSupportedPlatform(p.slug),
);
recentRoms.value = recents ?? [];
collections.value = cols ?? [];
smartCollections.value = smartCols ?? [];
virtualCollections.value = virtualCols ?? [];
collectionsStore.setCollections(cols ?? []);
collectionsStore.setFavoriteCollection(cols?.find((c) => c.is_favorite));
} catch (err: unknown) {
errorMessage.value = err instanceof Error ? err.message : "Failed to load";
} finally {
loadingPlatforms.value = false;
}
// Restore indices within bounds
if (platformIndex.value >= platforms.value.length) platformIndex.value = 0;
if (platformIndex.value >= allPlatforms.value.length) platformIndex.value = 0;
if (recentIndex.value >= recentRoms.value.length) recentIndex.value = 0;
if (collectionsIndex.value >= collections.value.length)
if (collectionsIndex.value >= allCollections.value.length)
collectionsIndex.value = 0;
if (smartCollectionsIndex.value >= smartCollections.value.length)
smartCollectionsIndex.value = 0;
@@ -723,7 +685,7 @@ onUnmounted(() => {
</div>
<div
v-if="loadingPlatforms"
v-if="fetchingPlatforms"
class="text-center mt-16"
:style="{ color: 'var(--console-loading-text)' }"
>
@@ -774,7 +736,7 @@ onUnmounted(() => {
>
<div class="flex items-center gap-6 h-full px-12 min-w-max">
<SystemCard
v-for="(p, i) in platforms"
v-for="(p, i) in allPlatforms"
:key="p.id"
:platform="p"
:index="i"
@@ -848,7 +810,7 @@ onUnmounted(() => {
</section>
<section
v-if="collections.length > 0"
v-if="allCollections.length > 0"
ref="collections-section-ref"
class="pb-8"
>
@@ -888,7 +850,7 @@ onUnmounted(() => {
>
<div class="flex items-center gap-4 h-full px-12 min-w-max">
<CollectionCard
v-for="(c, i) in collections"
v-for="(c, i) in allCollections"
:key="`collection-${c.id}`"
:collection="c"
:index="i"

View File

@@ -20,8 +20,12 @@ export default defineStore("console", {
navigationMode: "systems" as NavigationMode,
perPlatformGameIndex: {} as Record<number, number>,
perCollectionGameIndex: {} as Record<number, number>,
perSmartCollectionGameIndex: {} as Record<number, number>,
perVirtualCollectionGameIndex: {} as Record<string, number>,
perPlatformScrollTop: {} as Record<number, number>,
perCollectionScrollTop: {} as Record<number, number>,
perSmartCollectionScrollTop: {} as Record<number, number>,
perVirtualCollectionScrollTop: {} as Record<string, number>,
}),
getters: {
consoleMode: (state) => {
@@ -75,6 +79,18 @@ export default defineStore("console", {
getCollectionGameIndex(collectionId: number) {
return this.perCollectionGameIndex[collectionId] ?? 0;
},
setSmartCollectionGameIndex(smartCollectionId: number, idx: number) {
this.perSmartCollectionGameIndex[smartCollectionId] = idx;
},
getSmartCollectionGameIndex(smartCollectionId: number) {
return this.perSmartCollectionGameIndex[smartCollectionId] ?? 0;
},
setVirtualCollectionGameIndex(virtualCollectionId: string, idx: number) {
this.perVirtualCollectionGameIndex[virtualCollectionId] = idx;
},
getVirtualCollectionGameIndex(virtualCollectionId: string) {
return this.perVirtualCollectionGameIndex[virtualCollectionId] ?? 0;
},
setPlatformScroll(platformId: number, top: number) {
this.perPlatformScrollTop[platformId] = top;
},
@@ -87,5 +103,17 @@ export default defineStore("console", {
getCollectionScroll(collectionId: number) {
return this.perCollectionScrollTop[collectionId] ?? 0;
},
setSmartCollectionScroll(smartCollectionId: number, top: number) {
this.perSmartCollectionScrollTop[smartCollectionId] = top;
},
getSmartCollectionScroll(smartCollectionId: number) {
return this.perSmartCollectionScrollTop[smartCollectionId] ?? 0;
},
setVirtualCollectionScroll(virtualCollectionId: string, top: number) {
this.perVirtualCollectionScrollTop[virtualCollectionId] = top;
},
getVirtualCollectionScroll(virtualCollectionId: string) {
return this.perVirtualCollectionScrollTop[virtualCollectionId] ?? 0;
},
},
});