99// Metadata-provider links live in the Metadata tab, not the header.
1010// Genre/franchise belong in the Overview tab info grid.
1111import { RIcon , RPlatformIcon , RTag , RTooltip } from " @v2/lib" ;
12+ import { computed } from " vue" ;
1213import { useI18n } from " vue-i18n" ;
1314import type { DetailedRom } from " @/stores/roms" ;
1415import GameActions from " @/v2/components/GameActions/GameActions.vue" ;
1516import MainSiblingToggle from " @/v2/components/GameDetails/MainSiblingToggle.vue" ;
1617import PrevNextNav from " @/v2/components/GameDetails/PrevNextNav.vue" ;
18+ import { PROVIDERS } from " @/v2/components/GameDetails/providers" ;
1719import VersionSwitcher from " @/v2/components/GameDetails/VersionSwitcher.vue" ;
1820import { useGameActions } from " @/v2/composables/useGameActions" ;
1921
@@ -33,6 +35,88 @@ const props = defineProps<{
3335}>();
3436
3537const actions = useGameActions (() => props .rom );
38+
39+ type RatingChip = {
40+ key: " igdb_id" | " ss_id" | " moby_id" | " launchbox_id" | " hltb_id" ;
41+ name: string ;
42+ logo: string ;
43+ value: string ;
44+ };
45+
46+ function formatRating(value : number , options ? : { percent? : boolean }): string | null {
47+ if (! Number .isFinite (value )) return null ;
48+ const rounded = value >= 10 ? value .toFixed (0 ) : value .toFixed (1 );
49+ const formatted = rounded .replace (/ \. 0$ / , " " );
50+ return options ?.percent ? ` ${formatted }% ` : formatted ;
51+ }
52+
53+ function normalizeToTen(value : number ): number {
54+ if (! Number .isFinite (value )) return Number .NaN ;
55+ return value > 10 ? value / 10 : value ;
56+ }
57+
58+ function providerByKey(key : RatingChip [" key" ]) {
59+ return PROVIDERS .find ((p ) => p .key === key ) ?? null ;
60+ }
61+
62+ const visibleRegions = computed (() =>
63+ props .regions .filter ((r ) => r .trim ().toLowerCase () !== " world" ),
64+ );
65+
66+ const ratingChips = computed <RatingChip []>(() => {
67+ const rom = props .rom ;
68+
69+ const values: Array <{ key: RatingChip [" key" ]; value: string | null }> = [
70+ {
71+ key: " igdb_id" ,
72+ value: formatRating (
73+ normalizeToTen (parseFloat (rom .igdb_metadata ?.total_rating ?? " " )),
74+ ),
75+ },
76+ {
77+ key: " ss_id" ,
78+ value: formatRating (parseFloat (rom .ss_metadata ?.ss_score ?? " " ) * 10 , {
79+ percent: true ,
80+ }),
81+ },
82+ {
83+ key: " moby_id" ,
84+ value: formatRating (parseFloat (rom .moby_metadata ?.moby_score ?? " " ) * 10 , {
85+ percent: true ,
86+ }),
87+ },
88+ {
89+ key: " launchbox_id" ,
90+ value: formatRating (
91+ (rom .launchbox_metadata ?.community_rating ?? Number .NaN ) * 20 ,
92+ { percent: true },
93+ ),
94+ },
95+ {
96+ key: " hltb_id" ,
97+ value: formatRating (rom .hltb_metadata ?.review_score ?? Number .NaN , {
98+ percent: true ,
99+ }),
100+ },
101+ ];
102+
103+ const out: RatingChip [] = [];
104+
105+ for (const entry of values ) {
106+ if (! entry .value ) continue ;
107+ const provider = providerByKey (entry .key );
108+ if (! provider || ! provider .logo ) continue ;
109+
110+ out .push ({
111+ key: entry .key ,
112+ name: provider .name ,
113+ logo: provider .logo ,
114+ value: entry .value ,
115+ });
116+ }
117+
118+ return out ;
119+ });
36120 </script >
37121
38122<template >
@@ -82,18 +166,18 @@ const actions = useGameActions(() => props.rom);
82166 </span >
83167
84168 <span
85- v-if =" regions .length || languages.length || tags.length"
169+ v-if =" visibleRegions .length || languages.length || tags.length"
86170 class =" r-v2-det-header__sep"
87171 >
88172 ·
89173 </span >
90174
91175 <span
92- v-if =" regions .length || languages.length || tags.length"
176+ v-if =" visibleRegions .length || languages.length || tags.length"
93177 class =" r-v2-det-header__tags"
94178 >
95179 <RTag
96- v-for =" r in regions "
180+ v-for =" r in visibleRegions "
97181 :key =" ` r-${r } ` "
98182 :text =" r "
99183 tone="info"
@@ -108,6 +192,22 @@ const actions = useGameActions(() => props.rom);
108192 />
109193 <RTag v-for =" t in tags " :key =" ` t-${t } ` " :text =" t " size="small" />
110194 </span >
195+
196+ <span v-if =" ratingChips.length" class =" r-v2-det-header__ratings" >
197+ <span
198+ v-for =" chip in ratingChips"
199+ :key =" chip.key"
200+ class =" r-v2-det-header__rating-inline"
201+ :aria-label =" `${chip.name} rating ${chip.value}`"
202+ >
203+ <img
204+ class =" r-v2-det-header__rating-logo"
205+ :src =" chip.logo"
206+ :alt =" `${chip.name} logo`"
207+ />
208+ <span class =" r-v2-det-header__rating-value" >{{ chip.value }}</span >
209+ </span >
210+ </span >
111211 </div >
112212
113213 <div v-if =" rom.sibling_roms.length > 0" class =" r-v2-det-header__versions" >
@@ -183,6 +283,35 @@ const actions = useGameActions(() => props.rom);
183283 gap : 6px ;
184284}
185285
286+ .r-v2-det-header__ratings {
287+ display : inline-flex ;
288+ align-items : center ;
289+ flex-wrap : wrap ;
290+ gap : 6px ;
291+ flex-basis : 100% ;
292+ }
293+
294+ .r-v2-det-header__rating-inline {
295+ display : inline-flex ;
296+ align-items : center ;
297+ gap : 4px ;
298+ color : inherit ;
299+ }
300+
301+ .r-v2-det-header__rating-logo {
302+ inline-size : 20px ;
303+ block-size : 20px ;
304+ object-fit : contain ;
305+ flex-shrink : 0 ;
306+ }
307+
308+ .r-v2-det-header__rating-value {
309+ font-variant-numeric : tabular-nums ;
310+ color : inherit ;
311+ font-weight : inherit ;
312+ font-size : inherit ;
313+ }
314+
186315.r-v2-det-header__versions {
187316 display : flex ;
188317 align-items : center ;
0 commit comments