diff --git a/app/src/main/frontend/util/utils.ts b/app/src/main/frontend/util/utils.ts index 6b5dd9f..8c4cffb 100644 --- a/app/src/main/frontend/util/utils.ts +++ b/app/src/main/frontend/util/utils.ts @@ -241,6 +241,30 @@ function convertRange(value: number, originalRange: number[], targetRange: numbe return (value - originalRange[0]) * (targetRange[1] - targetRange[0]) / (originalRange[1] - originalRange[0]) + targetRange[0]; } +/** + * Calculate a compound rating for a GameDto based on its criticRating and userRating. + * If both ratings are present, a weighted average is calculated (40% critic, 60% user). + * If only one rating is present, that rating is returned. + * If neither rating is present, 0 is returned. + * @param game The GameDto object containing the ratings. + * @returns The compound rating as a number between 0 and 100. + */ +export function compoundRating(game: GameDto): number { + const weights = { + critic: 0.4, + user: 0.6 + }; + + const criticRating = game.criticRating ?? 0; + const userRating = game.userRating ?? 0; + + if (criticRating === 0 && userRating === 0) return 0; + if (criticRating === 0) return userRating; + if (userRating === 0) return criticRating; + + return Math.round((criticRating * weights.critic + userRating * weights.user) * 10) / 10; +} + /** * Convert a GameDto's ratings to a star rating out of 5. * If both criticRating and userRating are present, their average is taken. @@ -249,15 +273,11 @@ function convertRange(value: number, originalRange: number[], targetRange: numbe * @returns A string representing the star rating out of 5, or "N/A" if no ratings are available. */ export function gameRatingInStars(game: GameDto) { - if (!game.criticRating && !game.userRating) return "N/A"; - const originalRange = [0, 100]; const starRange = [1, 5]; - const ratings = []; - if (game.criticRating) ratings.push(game.criticRating); - if (game.userRating) ratings.push(game.userRating); - const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length; + const rating = compoundRating(game); + if (rating === 0) return "N/A"; - return convertRange(avgRating, originalRange, starRange).toFixed(1); + return convertRange(rating, originalRange, starRange).toFixed(1); } \ No newline at end of file diff --git a/app/src/main/frontend/views/SearchView.tsx b/app/src/main/frontend/views/SearchView.tsx index 0125b0d..e6ed454 100644 --- a/app/src/main/frontend/views/SearchView.tsx +++ b/app/src/main/frontend/views/SearchView.tsx @@ -9,7 +9,7 @@ import {Fzf} from "fzf"; import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto"; import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto"; import CoverGrid from "Frontend/components/general/covers/CoverGrid"; -import {gameRatingInStars, toTitleCase} from "Frontend/util/utils"; +import {compoundRating, toTitleCase} from "Frontend/util/utils"; export default function SearchView() { const games = useSnapshot(gameState).sortedAlphabetically as GameDto[]; @@ -138,7 +138,7 @@ export default function SearchView() { const [sortKey, sortDirection] = sortBy.split("_"); - return [...games].sort((a, b) => { + return games.slice().sort((a, b) => { let cmp: number; switch (sortKey) { @@ -149,7 +149,7 @@ export default function SearchView() { cmp = (a.release || "").localeCompare(b.release || ""); break; case "rating": - cmp = (a.criticRating ?? 0) - (b.criticRating ?? 0); + cmp = compoundRating(a) - compoundRating(b); break; case "added": cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); @@ -235,13 +235,11 @@ export default function SearchView() { // Apply minimum rating filter if (minRating > 1) { filtered = filtered.filter(game => { - const ratingStr = gameRatingInStars(game); - if (ratingStr === "N/A") return false; - const ratingNum = parseFloat(ratingStr); + const rating = compoundRating(game); if (minRating === 5) { - return ratingNum > 4.5; + return rating > 4.5; } - return ratingNum >= minRating; + return rating >= minRating; }); } return filtered;