instead of storing alt covers add toggle to theUI

This commit is contained in:
Georges-Antoine Assi
2025-10-23 15:58:00 -04:00
parent 304aa24b56
commit 0ccf644662
26 changed files with 197 additions and 70 deletions

View File

@@ -66,7 +66,6 @@ class Config:
SCAN_ARTWORK_PRIORITY: list[str]
SCAN_REGION_PRIORITY: list[str]
SCAN_LANGUAGE_PRIORITY: list[str]
SCAN_ARTWORK_COVER_STYLE: str
def __init__(self, **entries):
self.__dict__.update(entries)
@@ -243,11 +242,6 @@ class ConfigManager:
"scan.priority.language",
["en", "fr"],
),
SCAN_ARTWORK_COVER_STYLE=pydash.get(
self._raw_config,
"scan.artwork.cover_style",
"box2d",
).lower(),
)
def _get_ejs_controls(self) -> dict[str, EjsControls]:
@@ -426,12 +420,6 @@ class ConfigManager:
log.critical("Invalid config.yml: scan.priority.language must be a list")
sys.exit(3)
if not isinstance(self.config.SCAN_ARTWORK_COVER_STYLE, str):
log.critical(
"Invalid config.yml: scan.artwork.cover_style must be a string"
)
sys.exit(3)
def get_config(self) -> Config:
try:
with open(self.config_file, "r") as config_file:
@@ -487,9 +475,6 @@ class ConfigManager:
"region": self.config.SCAN_REGION_PRIORITY,
"language": self.config.SCAN_LANGUAGE_PRIORITY,
},
"artwork": {
"cover_style": self.config.SCAN_ARTWORK_COVER_STYLE,
},
},
}

View File

@@ -42,7 +42,6 @@ def get_config() -> ConfigResponse:
SCAN_ARTWORK_PRIORITY=cfg.SCAN_ARTWORK_PRIORITY,
SCAN_REGION_PRIORITY=cfg.SCAN_REGION_PRIORITY,
SCAN_LANGUAGE_PRIORITY=cfg.SCAN_LANGUAGE_PRIORITY,
SCAN_ARTWORK_COVER_STYLE=cfg.SCAN_ARTWORK_COVER_STYLE,
)

View File

@@ -22,4 +22,3 @@ class ConfigResponse(TypedDict):
SCAN_ARTWORK_PRIORITY: list[str]
SCAN_REGION_PRIORITY: list[str]
SCAN_LANGUAGE_PRIORITY: list[str]
SCAN_ARTWORK_COVER_STYLE: str

View File

@@ -7,7 +7,6 @@ from xml.etree.ElementTree import Element # trunk-ignore(bandit/B405)
import pydash
from defusedxml import ElementTree as ET
from config.config_manager import config_manager as cm
from handler.filesystem import fs_platform_handler
from logger.logger import log
from models.platform import Platform
@@ -50,12 +49,6 @@ class GamelistRom(BaseRom):
gamelist_metadata: NotRequired[GamelistMetadata]
def get_cover_style() -> str:
"""Get cover art style from config"""
config = cm.get_config()
return config.SCAN_ARTWORK_COVER_STYLE
def extract_media_from_gamelist_rom(game: Element) -> GamelistMetadataMedia:
image_elem = game.find("image")
video_elem = game.find("video")
@@ -273,7 +266,7 @@ class GamelistHandler(MetadataHandler):
)
# Choose which cover style to use
cover_path = rom_media.get(get_cover_style()) or rom_media["box2d"]
cover_path = rom_media["box2d"]
if cover_path:
cover_path_path = fs_platform_handler.validate_path(
f"{platform_dir}/{cover_path}"
@@ -299,7 +292,7 @@ class GamelistHandler(MetadataHandler):
f"{platform_dir}/{rom_media['title_screen']}"
)
url_screenshots.append(f"file://{str(title_screen_path)}")
if rom_media["miximage"] and get_cover_style() != "miximage":
if rom_media["miximage"]:
miximage_path = fs_platform_handler.validate_path(
f"{platform_dir}/{rom_media['miximage']}"
)

View File

@@ -41,12 +41,6 @@ def get_preferred_languages() -> list[str]:
return list(dict.fromkeys(config.SCAN_LANGUAGE_PRIORITY + ["en", "fr"]))
def get_cover_style() -> str:
"""Get cover art style from config"""
config = cm.get_config()
return config.SCAN_ARTWORK_COVER_STYLE
PS1_SS_ID: Final = 57
PS2_SS_ID: Final = 58
PSP_SS_ID: Final = 61
@@ -349,13 +343,13 @@ def build_ss_rom(game: SSGame) -> SSRom:
if res_summary:
break
url_cover = ss_media.get(get_cover_style()) or ss_media["box2d"]
url_cover = ss_media["box2d"]
url_manual = ss_media["manual"]
url_screenshots = pydash.compact(
[
ss_media["screenshot"],
ss_media["title_screen"],
ss_media["miximage"] if get_cover_style() != "miximage" else None,
ss_media["miximage"],
]
)

View File

@@ -45,8 +45,6 @@ scan:
language:
- jp
- es
artwork:
cover_style: box2d
emulatorjs:
debug: true

View File

@@ -80,8 +80,6 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
# language: # Cover art and game title (only used by Screenscraper)
# - "en"
# - "fr"
# artwork: # Only used by Screenscraper and ES-DE
# cover_style: box2d (Options: box2d, box3d, physical, miximage, fanart)
# EmulatorJS per-core options
# emulatorjs:

View File

@@ -22,6 +22,5 @@ export type ConfigResponse = {
SCAN_ARTWORK_PRIORITY: Array<string>;
SCAN_REGION_PRIORITY: Array<string>;
SCAN_LANGUAGE_PRIORITY: Array<string>;
SCAN_ARTWORK_COVER_STYLE: string;
};

View File

@@ -90,7 +90,11 @@ const coverImageSource = computed(() => {
const hostname = new URL(props.rom.url_cover).hostname;
if (hostname === "images.igdb.com") return "IGDB";
if (hostname === "screenscraper.fr") return "ScreenScraper";
if (
hostname === "screenscraper.fr" ||
hostname === "neoclone.screenscraper.fr"
)
return "ScreenScraper";
if (hostname === "cdn.mobygames.com" || hostname === "cdn2.mobygames.com")
return "MobyGames";
if (

View File

@@ -51,6 +51,9 @@ const enableExperimentalCacheRef = useLocalStorage(
false,
);
// Boxart
const boxartStyleRef = useLocalStorage("settings.boxartStyle", "box2d");
const homeOptions = computed(() => [
{
title: t("settings.show-stats"),
@@ -181,6 +184,14 @@ const galleryOptions = computed(() => [
},
]);
const boxartStyleOptions = computed(() => [
{ title: t("settings.boxart-box2d"), value: "box2d" },
{ title: t("settings.boxart-box3d"), value: "box3d" },
{ title: t("settings.boxart-physical"), value: "physical" },
{ title: t("settings.boxart-miximage"), value: "miximage" },
{ title: t("settings.boxart-fanart"), value: "fanart" },
]);
const setPlatformDrawerGroupBy = (value: string) => {
platformsGroupByRef.value = value;
};
@@ -200,19 +211,18 @@ const setVirtualCollectionType = async (value: string) => {
virtualCollectionTypeRef.value = value;
collectionsStore.fetchVirtualCollections(value);
};
const setBoxartStyle = (value: string) => {
boxartStyleRef.value = value;
};
const toggleShowStats = (value: boolean) => {
showStatsRef.value = value;
};
const toggleShowRecentRoms = (value: boolean) => {
showRecentRomsRef.value = value;
};
const toggleGroupRoms = (value: boolean) => {
groupRomsRef.value = value;
};
const toggleSiblings = (value: boolean) => {
siblingsRef.value = value;
};
@@ -220,15 +230,12 @@ const toggleSiblings = (value: boolean) => {
const toggleRegions = (value: boolean) => {
regionsRef.value = value;
};
const toggleLanguages = (value: boolean) => {
languagesRef.value = value;
};
const toggleStatus = (value: boolean) => {
statusRef.value = value;
};
const toggleActionBar = (value: boolean) => {
actionBarRef.value = value;
};
@@ -271,6 +278,17 @@ const toggleExperimentalCache = (value: boolean) => {
@update:model-value="option.modelTrigger"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="boxartStyleRef"
:items="boxartStyleOptions"
:label="t('settings.boxart-style')"
class="mx-2 mt-2"
variant="outlined"
hide-details
@update:model-value="setBoxartStyle"
/>
</v-col>
</v-row>
<v-chip
label

View File

@@ -124,6 +124,9 @@ const activeMenu = ref(false);
const showActionBarAlways = useLocalStorage("settings.showActionBar", false);
const showGameTitleAlways = useLocalStorage("settings.showGameTitle", false);
const showSiblings = useLocalStorage("settings.showSiblings", true);
const boxartStyle = useLocalStorage<
"box2d" | "box3d" | "physical" | "miximage" | "fanart"
>("settings.boxartStyle", "box2d");
const hasNotes = computed(() => {
if (!romsStore.isSimpleRom(props.rom)) return false;
@@ -146,9 +149,23 @@ const isWebpEnabled = computed(
() => heartbeatStore.value.TASKS?.ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP,
);
// User selected alternative cover image
const boxartStyleCover = computed(() => {
if (
props.coverSrc ||
!romsStore.isSimpleRom(props.rom) ||
boxartStyle.value === "box2d"
)
return null;
const ssMedia = props.rom.ss_metadata?.[boxartStyle.value];
const gamelistMedia = props.rom.gamelist_metadata?.[boxartStyle.value];
return ssMedia || gamelistMedia;
});
const largeCover = computed(() => {
if (props.coverSrc) return props.coverSrc;
if (!romsStore.isSimpleRom(props.rom))
if (boxartStyleCover.value) return boxartStyleCover.value;
if (!romsStore.isSimpleRom(props.rom)) {
return (
props.rom.igdb_url_cover ||
props.rom.moby_url_cover ||
@@ -156,6 +173,7 @@ const largeCover = computed(() => {
props.rom.launchbox_url_cover ||
props.rom.flashpoint_url_cover
);
}
const pathCoverLarge = isWebpEnabled.value
? props.rom.path_cover_large?.replace(EXTENSION_REGEX, ".webp")
: props.rom.path_cover_large;
@@ -164,6 +182,7 @@ const largeCover = computed(() => {
const smallCover = computed(() => {
if (props.coverSrc) return props.coverSrc;
if (boxartStyleCover.value) return boxartStyleCover.value;
if (!romsStore.isSimpleRom(props.rom)) return "";
const pathCoverSmall = isWebpEnabled.value
? props.rom.path_cover_small?.replace(EXTENSION_REGEX, ".webp")
@@ -171,14 +190,6 @@ const smallCover = computed(() => {
return pathCoverSmall || "";
});
const is3DCover = computed(() => {
if (!romsStore.isSimpleRom(props.rom)) return false;
return (
props.rom.url_cover?.includes("box-3D") ||
props.rom.url_cover?.includes("3dboxes")
);
});
const showNoteDialog = (event: MouseEvent | KeyboardEvent) => {
event.preventDefault();
if (romsStore.isSimpleRom(props.rom)) {
@@ -232,7 +243,9 @@ onBeforeUnmount(() => {
'border-selected': withBorderPrimary,
'transform-scale': transformScale && !enable3DTilt,
}"
:elevation="isOuterHovering && transformScale ? 20 : 3"
:elevation="
isOuterHovering && transformScale ? 20 : boxartStyleCover ? 0 : 3
"
:aria-label="`${rom.name} game card`"
@mouseenter="
() => {
@@ -255,8 +268,8 @@ onBeforeUnmount(() => {
<v-img
v-bind="imgProps"
:key="romsStore.isSimpleRom(rom) ? rom.id : rom.name"
:cover="!is3DCover"
:contain="is3DCover"
:cover="!boxartStyleCover"
:contain="boxartStyleCover"
content-class="d-flex flex-column justify-space-between"
:class="{ pointer: pointerOnHover }"
:src="largeCover || fallbackCoverImage"
@@ -309,7 +322,7 @@ onBeforeUnmount(() => {
/>
</v-col>
</v-row>
<div v-bind="props">
<div>
<v-row
v-if="romsStore.isSimpleRom(rom) && showChips"
no-gutters
@@ -391,8 +404,8 @@ onBeforeUnmount(() => {
</div>
<template #placeholder>
<v-img
:cover="!is3DCover"
:contain="is3DCover"
:cover="!boxartStyleCover"
:contain="boxartStyleCover"
eager
:src="smallCover || fallbackCoverImage"
:aspect-ratio="computedAspectRatio"
@@ -408,8 +421,8 @@ onBeforeUnmount(() => {
</template>
<template #error>
<v-img
:cover="!is3DCover"
:contain="is3DCover"
:cover="!boxartStyleCover"
:contain="boxartStyleCover"
eager
:src="fallbackCoverImage"
:aspect-ratio="computedAspectRatio"

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useLocalStorage } from "@vueuse/core";
import { computed, onMounted, useTemplateRef, watch } from "vue";
import Skeleton from "@/components/common/Game/Card/Skeleton.vue";
import {
@@ -7,7 +8,7 @@ import {
} from "@/console/composables/useElementRegistry";
import storeCollections from "@/stores/collections";
import storeHeartbeat from "@/stores/heartbeat";
import type { SimpleRom } from "@/stores/roms";
import storeRoms, { type SimpleRom } from "@/stores/roms";
import {
EXTENSION_REGEX,
getMissingCoverImage,
@@ -24,12 +25,26 @@ const props = defineProps<{
}>();
const heartbeatStore = storeHeartbeat();
const romsStore = storeRoms();
const boxartStyle = useLocalStorage<
"box2d" | "box3d" | "physical" | "miximage" | "fanart"
>("settings.boxartStyle", "box2d");
const isWebpEnabled = computed(
() => heartbeatStore.value.TASKS?.ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP,
);
// User selected alternative cover image
const boxartStyleCover = computed(() => {
if (boxartStyle.value === "box2d") return null;
const ssMedia = props.rom.ss_metadata?.[boxartStyle.value];
const gamelistMedia = props.rom.gamelist_metadata?.[boxartStyle.value];
return ssMedia || gamelistMedia;
});
const largeCover = computed(() => {
if (boxartStyleCover.value) return boxartStyleCover.value;
const pathCoverLarge = isWebpEnabled.value
? props.rom.path_cover_large?.replace(EXTENSION_REGEX, ".webp")
: props.rom.path_cover_large;
@@ -37,6 +52,7 @@ const largeCover = computed(() => {
});
const smallCover = computed(() => {
if (boxartStyleCover.value) return boxartStyleCover.value;
const pathCoverSmall = isWebpEnabled.value
? props.rom.path_cover_small?.replace(EXTENSION_REGEX, ".webp")
: props.rom.path_cover_small;
@@ -95,26 +111,34 @@ onMounted(() => {
<template>
<button
ref="game-card-ref"
class="relative block border-2 border-white/10 rounded-md p-0 cursor-pointer overflow-hidden shadow-[0_4px_20px_rgba(0,0,0,0.3),_inset_0_1px_0_rgba(255,255,255,0.1)] transition-all duration-200"
class="relative block border-2 border-white/10 rounded-md p-0 cursor-pointer overflow-hidden transition-all duration-200"
:class="{
'-translate-y-[2px] scale-[1.03] shadow-[0_8px_28px_rgba(0,0,0,0.35),_0_0_0_2px_var(--console-game-card-focus-border),_0_0_16px_var(--console-game-card-focus-border)]':
selected,
'w-[250px] shrink-0': continuePlaying,
'shadow-[0_4px_20px_rgba(0,0,0,0.3),_inset_0_1px_0_rgba(255,255,255,0.1)]':
!boxartStyleCover,
}"
@click="emit('click')"
@focus="emit('focus')"
>
<div class="w-full h-[350px] relative overflow-hidden rounded">
<v-img
cover
class="w-full h-full object-cover"
class="w-full h-full"
:cover="!boxartStyleCover"
:contain="boxartStyleCover"
:src="largeCover || fallbackCoverImage"
:alt="rom.name || 'Game'"
@load="emit('loaded')"
@error="emit('loaded')"
>
<template #placeholder>
<v-img cover eager :src="smallCover || fallbackCoverImage">
<v-img
eager
:src="smallCover || fallbackCoverImage"
:cover="!boxartStyleCover"
:contain="boxartStyleCover"
>
<template #placeholder>
<Skeleton :platform-id="rom.platform_id" type="image" />
</template>

View File

@@ -1,4 +1,12 @@
{
"backlogged": "Rückstand",
"boxart-style": "Boxart-Stil",
"boxart-desc": "Wählen Sie den Boxart-Stil für Spielkarten",
"boxart-box2d": "2D-Box",
"boxart-box3d": "3D-Box",
"boxart-fanart": "Fanart",
"boxart-miximage": "Mix-Bild",
"boxart-physical": "Physisch",
"canceled": "Abgebrochen",
"cleanup": "Bereinigung",
"completed": "Abgeschlossen",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "Backlogged",
"boxart-style": "Boxart style",
"boxart-desc": "Choose the boxart style for game cards",
"boxart-box2d": "2D Box",
"boxart-box3d": "3D Box",
"boxart-fanart": "Fanart",
"boxart-miximage": "Mix Image",
"boxart-physical": "Physical",
"canceled": "Canceled",
"cleanup": "Cleanup",
"completed": "Completed",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "Pendiente",
"boxart-style": "Estilo de carátula",
"boxart-desc": "Elige el estilo de carátula para las tarjetas de juegos",
"boxart-box2d": "Caja 2D",
"boxart-box3d": "Caja 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Imagen mixta",
"boxart-physical": "Físico",
"canceled": "Cancelado",
"cleanup": "Limpieza",
"completed": "Completado",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "En attente",
"boxart-style": "Style de jaquette",
"boxart-desc": "Choisissez le style de jaquette pour les cartes de jeux",
"boxart-box2d": "Boîte 2D",
"boxart-box3d": "Boîte 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Image mixte",
"boxart-physical": "Physique",
"canceled": "Annulé",
"cleanup": "Nettoyage",
"completed": "Terminé",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "In sospeso",
"boxart-style": "Stile copertina",
"boxart-desc": "Scegli lo stile della copertina per le carte dei giochi",
"boxart-box2d": "Scatola 2D",
"boxart-box3d": "Scatola 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Immagine mista",
"boxart-physical": "Fisico",
"canceled": "Annullato",
"cleanup": "Pulizia",
"completed": "Completato",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "バックログ",
"boxart-style": "ボックスアートスタイル",
"boxart-desc": "ゲームカードのボックスアートスタイルを選択",
"boxart-box2d": "2Dボックス",
"boxart-box3d": "3Dボックス",
"boxart-fanart": "ファンアート",
"boxart-miximage": "ミックス画像",
"boxart-physical": "物理",
"canceled": "キャンセル済み",
"cleanup": "クリーンアップ",
"completed": "完了済み",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "백로그",
"boxart-style": "박스아트 스타일",
"boxart-desc": "게임 카드의 박스아트 스타일을 선택하세요",
"boxart-box2d": "2D 박스",
"boxart-box3d": "3D 박스",
"boxart-fanart": "팬아트",
"boxart-miximage": "믹스 이미지",
"boxart-physical": "물리적",
"canceled": "취소됨",
"cleanup": "정리",
"completed": "완료됨",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "Zaległości",
"boxart-style": "Styl okładki",
"boxart-desc": "Wybierz styl okładki dla kart gier",
"boxart-box2d": "Pudełko 2D",
"boxart-box3d": "Pudełko 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Obraz mieszany",
"boxart-physical": "Fizyczny",
"canceled": "Anulowano",
"cleanup": "Czyszczenie",
"completed": "Ukończono",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "Pendente",
"boxart-style": "Estilo da capa",
"boxart-desc": "Escolha o estilo da capa para os cartões de jogos",
"boxart-box2d": "Caixa 2D",
"boxart-box3d": "Caixa 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Imagem mista",
"boxart-physical": "Físico",
"canceled": "Cancelado",
"cleanup": "Limpeza",
"completed": "Concluído",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "În așteptare",
"boxart-style": "Stilul copertii",
"boxart-desc": "Alege stilul copertii pentru cardurile de jocuri",
"boxart-box2d": "Cutie 2D",
"boxart-box3d": "Cutie 3D",
"boxart-fanart": "Fanart",
"boxart-miximage": "Imagine mixtă",
"boxart-physical": "Fizic",
"canceled": "Anulat",
"cleanup": "Curățare",
"completed": "Finalizat",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "В очереди",
"boxart-style": "Стиль обложки",
"boxart-desc": "Выберите стиль обложки для карточек игр",
"boxart-box2d": "2D коробка",
"boxart-box3d": "3D коробка",
"boxart-fanart": "Фанарт",
"boxart-miximage": "Смешанное изображение",
"boxart-physical": "Физический",
"canceled": "Отменено",
"cleanup": "Очистка",
"completed": "Завершено",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "待办",
"boxart-style": "封面样式",
"boxart-desc": "选择游戏卡片的封面样式",
"boxart-box2d": "2D盒子",
"boxart-box3d": "3D盒子",
"boxart-fanart": "粉丝艺术",
"boxart-miximage": "混合图像",
"boxart-physical": "物理",
"canceled": "已取消",
"cleanup": "清理",
"completed": "已完成",

View File

@@ -1,4 +1,12 @@
{
"backlogged": "待辦",
"boxart-style": "封面樣式",
"boxart-desc": "選擇遊戲卡片的封面樣式",
"boxart-box2d": "2D盒子",
"boxart-box3d": "3D盒子",
"boxart-fanart": "粉絲藝術",
"boxart-miximage": "混合圖像",
"boxart-physical": "物理",
"canceled": "已取消",
"cleanup": "清理",
"completed": "已完成",

View File

@@ -30,7 +30,6 @@ const defaultConfig = {
SCAN_ARTWORK_PRIORITY: [],
SCAN_REGION_PRIORITY: [],
SCAN_LANGUAGE_PRIORITY: [],
SCAN_ARTWORK_COVER_STYLE: "",
} as ConfigResponse;
export default defineStore("config", {