Merge pull request #1962 from rommapp/home-fallback-empty

Add fallback for homepage when library is empty
This commit is contained in:
Georges-Antoine Assi
2025-06-10 19:26:40 -04:00
committed by GitHub
20 changed files with 164 additions and 95 deletions

View File

@@ -53,14 +53,14 @@ function onHover(emitData: { isHovering: boolean; id: number }) {
:class="{
'flex-nowrap overflow-x-auto': !gridCollections,
}"
class="pa-1"
class="py-1"
no-gutters
style="overflow-y: hidden"
>
<v-col
v-for="collection in collections.allCollections"
:key="collection.name"
class="pa-1 my-4"
class="pa-1"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
:md="views[0]['size-md']"

View File

@@ -69,14 +69,14 @@ function onClosedMenu() {
:class="{
'flex-nowrap overflow-x-auto': !gridContinuePlayingRoms,
}"
class="pa-1"
class="py-1"
no-gutters
style="overflow-y: hidden"
>
<v-col
v-for="rom in continuePlayingRoms"
:key="rom.id"
class="pa-1 align-self-end my-4"
class="pa-1 align-self-end"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
:md="views[0]['size-md']"

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>
<template>
<v-empty-state
:headline="t('emptyStates.home-headline')"
:title="t('emptyStates.home-title')"
:text="t('emptyStates.home-text')"
/>
</template>

View File

@@ -54,14 +54,14 @@ function onHover(emitData: { isHovering: boolean; id: number }) {
:class="{
'flex-nowrap overflow-x-auto': !gridPlatforms,
}"
class="pa-1"
class="py-1"
no-gutters
style="overflow-y: hidden"
>
<v-col
v-for="platform in filledPlatforms"
:key="platform.slug"
class="pa-1 my-4"
class="pa-1"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
:md="views[0]['size-md']"

View File

@@ -67,14 +67,14 @@ function onClosedMenu() {
:class="{
'flex-nowrap overflow-x-auto': !gridRecentRoms,
}"
class="pa-1"
class="py-1"
no-gutters
style="overflow-y: hidden"
>
<v-col
v-for="rom in recentRoms"
:key="rom.id"
class="pa-1 align-self-end my-4"
class="pa-1 align-self-end"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
:md="views[0]['size-md']"

View File

@@ -74,7 +74,7 @@ onBeforeUnmount(() => {
:class="{
'flex-nowrap overflow-x-auto': !gridVirtualCollections,
}"
class="pa-1"
class="py-1"
no-gutters
style="overflow-y: hidden"
>
@@ -84,7 +84,7 @@ onBeforeUnmount(() => {
visibleCollections,
)"
:key="collection.name"
class="pa-1 my-4"
class="pa-1"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
:md="views[0]['size-md']"

View File

@@ -2,5 +2,8 @@
"404-subtitle": "Die Seite, die Sie suchen, existiert nicht",
"404-title": "Seite nicht gefunden",
"search-for-games": "Suche nach Spielen auf allen Plattformen",
"manual-match": "Suche nach Spielen bei allen Metadatenanbietern"
"manual-match": "Suche nach Spielen bei allen Metadatenanbietern",
"home-headline": "Keine Spiele in Ihrer Bibliothek",
"home-title": "Führen Sie einen Schnellscan durch, um Spiele auf Ihrem System automatisch zu erkennen",
"home-text": "Bereit zum Spielen?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "The page you are looking for does not exist",
"404-title": "Page not found",
"search-for-games": "Search for games across all platforms",
"manual-match": "Search for games across all metadata providers"
"manual-match": "Search for games across all metadata providers",
"home-headline": "No games in your library",
"home-title": "Run a quick scan to automatically detect games on your system",
"home-text": "Ready to play?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "The page you are looking for does not exist",
"404-title": "Page not found",
"search-for-games": "Search for games across all platforms",
"manual-match": "Search for games across all metadata providers"
"manual-match": "Search for games across all metadata providers",
"home-headline": "No games in your library",
"home-title": "Run a quick scan to automatically detect games on your system",
"home-text": "Ready to play?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "La página que buscas no existe",
"404-title": "Página no encontrada",
"search-for-games": "Busca juegos en todas las plataformas",
"manual-match": "Busca juegos en todos los proveedores de metadatos"
"manual-match": "Busca juegos en todos los proveedores de metadatos",
"home-headline": "No hay juegos en tu biblioteca",
"home-title": "Ejecuta un escaneo rápido para detectar juegos en tu sistema automáticamente",
"home-text": "¿Listo para jugar?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "La page que vous recherchez n'existe pas",
"404-title": "Page introuvable",
"search-for-games": "Recherchez des jeux sur toutes les plateformes",
"manual-match": "Recherchez des jeux auprès de tous les fournisseurs de métadonnées"
"manual-match": "Recherchez des jeux auprès de tous les fournisseurs de métadonnées",
"home-headline": "Aucun jeu dans votre bibliothèque",
"home-title": "Effectuez une analyse rapide pour détecter automatiquement les jeux sur votre système",
"home-text": "Prêt à jouer ?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "La pagina che stai cercando non esiste",
"404-title": "Pagina non trovata",
"search-for-games": "Cerca giochi su tutte le piattaforme",
"manual-match": "Cerca giochi su tutti i fornitori di metadati"
"manual-match": "Cerca giochi su tutti i fornitori di metadati",
"home-headline": "Nessun gioco nella tua libreria",
"home-title": "Esegui una scansione rapida per rilevare automaticamente i giochi sul tuo sistema",
"home-text": "Pronto a giocare?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "表示しようとしたページは見つかりませんでした",
"404-title": "ページが見つかりませんでした",
"search-for-games": "全てのプラットフォームから検索",
"manual-match": "全てのメタデータプロバイダーから検索"
"manual-match": "全てのメタデータプロバイダーから検索",
"home-headline": "ライブラリにゲームがありません",
"home-title": "システム上のゲームを自動的に検出するためにクイックスキャンを実行します",
"home-text": "プレイする準備はできましたか?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "찾으려고 하는 페이지가 존재하지 않습니다",
"404-title": "페이지 찾을 수 없음",
"search-for-games": "모든 기종에서 게임 찾기",
"manual-match": "모든 메타데이터 제공업체에서 게임 찾기"
"manual-match": "모든 메타데이터 제공업체에서 게임 찾기",
"home-headline": "라이브러리에 게임이 없습니다",
"home-title": "시스템에서 게임을 자동으로 감지하기 위해 빠른 스캔을 실행하세요",
"home-text": "게임을 할 준비가 되셨나요?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "A página que você está procurando não existe",
"404-title": "Página não encontrada",
"search-for-games": "Pesquisar jogos em todas as plataformas",
"manual-match": "Pesquisar jogos em todos os provedores de metadados"
"manual-match": "Pesquisar jogos em todos os provedores de metadados",
"home-headline": "Nenhum jogo na sua biblioteca",
"home-title": "Execute uma varredura rápida para detectar jogos no seu sistema automaticamente",
"home-text": "Pronto para jogar?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "Pagina pe care o căutați nu există",
"404-title": "Pagină negăsită",
"search-for-games": "Căutați jocuri pe toate platformele",
"manual-match": "Căutați jocuri la toți furnizorii de metadate"
"manual-match": "Căutați jocuri la toți furnizorii de metadate",
"home-headline": "Nu există jocuri în biblioteca dvs.",
"home-title": "Rulați o scanare rapidă pentru a detecta automat jocurile de pe sistemul dvs.",
"home-text": "Sunteți gata de joacă?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "Страница, которую вы ищете, не существует",
"404-title": "Страница не найдена",
"search-for-games": "Искать игры на всех платформах",
"manual-match": "Искать игры у всех поставщиков метаданных"
"manual-match": "Искать игры у всех поставщиков метаданных",
"home-headline": "В вашей библиотеке нет игр",
"home-title": "Запустите быструю проверку, чтобы автоматически обнаружить игры на вашей системе",
"home-text": "Готовы играть?"
}

View File

@@ -2,5 +2,8 @@
"404-subtitle": "找不到页面",
"404-title": "页面未找到",
"search-for-games": "搜索游戏",
"manual-match": "在所有元数据提供商中搜索游戏"
"manual-match": "在所有元数据提供商中搜索游戏",
"home-headline": "您的库中没有游戏",
"home-title": "运行快速扫描以自动检测系统上的游戏",
"home-text": "准备好游戏了吗?"
}

View File

@@ -337,6 +337,7 @@ export function languageToEmoji(language: string) {
*/
const _EJS_CORES_MAP = {
"3do": ["opera"],
acpc: ["cap32", "crocods"],
amiga: ["puae"],
"amiga-cd32": ["puae"],
arcade: [
@@ -359,6 +360,7 @@ const _EJS_CORES_MAP = {
c128: ["vice_x128"],
"commmodore-128": ["vice_x128"],
colecovision: ["gearcoleco"],
doom: ["prboom"],
jaguar: ["virtualjaguar"],
lynx: ["handy"],
"atari-lynx-mkii": ["handy"],
@@ -413,6 +415,7 @@ const _EJS_CORES_MAP = {
wonderswan: ["mednafen_wswan"],
swancrystal: ["mednafen_wswan"],
"wonderswan-color": ["mednafen_wswan"],
"zx-spectrum": ["fuse"],
} as const;
export type EJSPlatformSlug = keyof typeof _EJS_CORES_MAP;

View File

@@ -1,109 +1,126 @@
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import Collections from "@/components/Home/Collections.vue";
import VirtualCollections from "@/components/Home/VirtualCollections.vue";
import Platforms from "@/components/Home/Platforms.vue";
import RecentSkeletonLoader from "@/components/Home/RecentSkeletonLoader.vue";
import RecentAdded from "@/components/Home/RecentAdded.vue";
import ContinuePlaying from "@/components/Home/ContinuePlaying.vue";
import EmptyHome from "@/components/Home/EmptyHome.vue";
import romApi from "@/services/api/rom";
import storeCollections from "@/stores/collections";
import storePlatforms from "@/stores/platforms";
import storeRoms from "@/stores/roms";
import { storeToRefs } from "pinia";
import { onMounted, ref } from "vue";
import { isNull } from "lodash";
import { useI18n } from "vue-i18n";
// Props
const { t } = useI18n();
const romsStore = storeRoms();
const { recentRoms, continuePlayingRoms: recentPlayedRoms } =
storeToRefs(romsStore);
const platforms = storePlatforms();
const { filledPlatforms } = storeToRefs(platforms);
const collections = storeCollections();
const { allCollections, virtualCollections } = storeToRefs(collections);
const showRecentRoms = isNull(localStorage.getItem("settings.showRecentRoms"))
? true
: localStorage.getItem("settings.showRecentRoms") === "true";
const showContinuePlaying = isNull(
localStorage.getItem("settings.showContinuePlaying"),
)
? true
: localStorage.getItem("settings.showContinuePlaying") === "true";
const showPlatforms = isNull(localStorage.getItem("settings.showPlatforms"))
? true
: localStorage.getItem("settings.showPlatforms") === "true";
const showCollections = isNull(localStorage.getItem("settings.showCollections"))
? true
: localStorage.getItem("settings.showCollections") === "true";
const showVirtualCollections = isNull(
localStorage.getItem("settings.showVirtualCollections"),
)
? true
: localStorage.getItem("settings.showVirtualCollections") === "true";
const platformsStore = storePlatforms();
const { filledPlatforms } = storeToRefs(platformsStore);
const collectionsStore = storeCollections();
const { allCollections, virtualCollections } = storeToRefs(collectionsStore);
function getSettingValue(key: string, defaultValue: boolean = true): boolean {
const stored = localStorage.getItem(`settings.${key}`);
return stored === null ? defaultValue : stored === "true";
}
const showRecentRoms = getSettingValue("showRecentRoms");
const showContinuePlaying = getSettingValue("showContinuePlaying");
const showPlatforms = getSettingValue("showPlatforms");
const showCollections = getSettingValue("showCollections");
const showVirtualCollections = getSettingValue("showVirtualCollections");
const fetchingRecentAdded = ref(false);
const fetchingContinuePlaying = ref(false);
// Functions
onMounted(async () => {
fetchingRecentAdded.value = true;
fetchingContinuePlaying.value = true;
romApi
.getRecentRoms()
.then(({ data: { items } }) => {
romsStore.setRecentRoms(items);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
fetchingRecentAdded.value = false;
});
const isEmpty = computed(
() =>
recentRoms.value.length === 0 &&
recentPlayedRoms.value.length === 0 &&
filledPlatforms.value.length === 0 &&
allCollections.value.length === 0 &&
virtualCollections.value.length === 0,
);
romApi
.getRecentPlayedRoms()
.then(({ data: { items } }) => {
romsStore.setContinuePlayedRoms(
items.filter((rom) => rom.rom_user.last_played),
);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
fetchingContinuePlaying.value = false;
});
const showRecentSkeleton = computed(
() =>
showRecentRoms &&
fetchingRecentAdded.value &&
recentRoms.value.length === 0,
);
const showContinuePlayingSkeleton = computed(
() =>
showContinuePlaying &&
fetchingContinuePlaying.value &&
recentPlayedRoms.value.length === 0,
);
const fetchRecentRoms = async (): Promise<void> => {
try {
fetchingRecentAdded.value = true;
const {
data: { items },
} = await romApi.getRecentRoms();
romsStore.setRecentRoms(items);
} catch (error) {
console.error("Failed to fetch recent ROMs:", error);
} finally {
fetchingRecentAdded.value = false;
}
};
const fetchContinuePlayingRoms = async (): Promise<void> => {
try {
fetchingContinuePlaying.value = true;
const {
data: { items },
} = await romApi.getRecentPlayedRoms();
const filteredItems = items.filter((rom) => rom.rom_user.last_played);
romsStore.setContinuePlayedRoms(filteredItems);
} catch (error) {
console.error("Failed to fetch continue playing ROMs:", error);
} finally {
fetchingContinuePlaying.value = false;
}
};
onMounted(async () => {
await Promise.all([fetchRecentRoms(), fetchContinuePlayingRoms()]);
});
</script>
<template>
<recent-skeleton-loader
v-if="showRecentRoms && fetchingRecentAdded && recentRoms.length === 0"
<RecentSkeletonLoader
v-if="showRecentSkeleton"
:title="t('home.recently-added')"
class="ma-2"
/>
<recent-added class="ma-2" v-if="recentRoms.length > 0 && showRecentRoms" />
<recent-skeleton-loader
v-if="
showContinuePlaying &&
fetchingContinuePlaying &&
recentPlayedRoms.length === 0
"
<RecentAdded
v-else-if="recentRoms.length > 0 && showRecentRoms"
class="ma-2"
/>
<RecentSkeletonLoader
v-if="showContinuePlayingSkeleton"
:title="t('home.continue-playing')"
class="ma-2"
/>
<continue-playing
<ContinuePlaying
v-else-if="recentPlayedRoms.length > 0 && showContinuePlaying"
class="ma-2"
v-if="recentPlayedRoms.length > 0 && showContinuePlaying"
/>
<platforms class="ma-2" v-if="filledPlatforms.length > 0 && showPlatforms" />
<collections
class="ma-2"
<Platforms v-if="filledPlatforms.length > 0 && showPlatforms" class="ma-2" />
<Collections
v-if="allCollections.length > 0 && showCollections"
/>
<virtual-collections
class="ma-2"
v-if="virtualCollections.length > 0 && showVirtualCollections"
/>
<VirtualCollections
v-if="virtualCollections.length > 0 && showVirtualCollections"
class="ma-2"
/>
<EmptyHome v-if="isEmpty" class="ma-2" />
</template>