Compare commits

...

12 Commits

Author SHA1 Message Date
Moyase
4e912b3b8d Merge branch 'main' into fix/game_asset_changing_path 2025-10-12 22:52:45 +03:00
Moyasee
e71211f1aa Fix: extracted ternary operations 2025-10-12 22:51:35 +03:00
Moyasee
a946f3bd5a Fix: extracted ternary operations 2025-10-12 22:48:33 +03:00
Chubby Granny Chaser
374b62983b feat: adding 2h constraint 2025-10-12 20:48:13 +01:00
Moyasee
0cd4c3ccf6 Fix: extracted ternary operations 2025-10-12 22:45:20 +03:00
Moyasee
7b97663b3a Merge branch 'fix/game_asset_changing_path' of https://github.com/hydralauncher/hydra into fix/game_asset_changing_path 2025-10-12 22:40:07 +03:00
Moyasee
68e2e2a772 Fix: conditional structure 2025-10-12 22:39:26 +03:00
Moyase
39979292e2 Merge branch 'main' into fix/game_asset_changing_path 2025-10-12 22:37:28 +03:00
Moyasee
60ae7d40fa Fix: Image path persists upon clearing image 2025-10-12 22:34:05 +03:00
Chubby Granny Chaser
63b6b0b44e Merge pull request #1805 from hydralauncher/feat/reviews-and-commenting
fix: fixing search on sources modal
2025-10-12 20:17:22 +01:00
Moyasee
82c0dc0d97 Merge branch 'main' of https://github.com/hydralauncher/hydra into fix/game_asset_changing_path 2025-10-12 22:13:09 +03:00
Moyasee
1cba3f350c formatting 2025-10-12 22:12:15 +03:00
2 changed files with 384 additions and 257 deletions

View File

@@ -1,4 +1,11 @@
import { useContext, useEffect, useMemo, useRef, useState } from "react"; import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
PencilIcon, PencilIcon,
TrashIcon, TrashIcon,
@@ -164,6 +171,8 @@ export function GameDetailsContent() {
const [hasUserReviewed, setHasUserReviewed] = useState(false); const [hasUserReviewed, setHasUserReviewed] = useState(false);
const [reviewCheckLoading, setReviewCheckLoading] = useState(false); const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
// Check if the current game is in the user's library // Check if the current game is in the user's library
const isGameInLibrary = useMemo(() => { const isGameInLibrary = useMemo(() => {
if (!library || !shop || !objectId) return false; if (!library || !shop || !objectId) return false;
@@ -225,6 +234,14 @@ export function GameDetailsContent() {
useEffect(() => { useEffect(() => {
setBackdropOpacity(1); setBackdropOpacity(1);
// Cleanup: abort any pending review requests when objectId changes
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
};
}, [objectId]); }, [objectId]);
const handleCloudSaveButtonClick = () => { const handleCloudSaveButtonClick = () => {
@@ -256,7 +273,7 @@ export function GameDetailsContent() {
const isCustomGame = game?.shop === "custom"; const isCustomGame = game?.shop === "custom";
const checkUserReview = async () => { const checkUserReview = useCallback(async () => {
if (!objectId || !userDetails) return; if (!objectId || !userDetails) return;
setReviewCheckLoading(true); setReviewCheckLoading(true);
@@ -265,51 +282,77 @@ export function GameDetailsContent() {
const hasReviewed = (response as any)?.hasReviewed || false; const hasReviewed = (response as any)?.hasReviewed || false;
setHasUserReviewed(hasReviewed); setHasUserReviewed(hasReviewed);
const twoHoursInMilliseconds = 2 * 60 * 60 * 1000;
const hasEnoughPlaytime =
game &&
game.playTimeInMilliseconds >= twoHoursInMilliseconds &&
!game.hasManuallyUpdatedPlaytime;
if ( if (
!hasReviewed && !hasReviewed &&
hasEnoughPlaytime &&
!sessionStorage.getItem(`reviewPromptDismissed_${objectId}`) !sessionStorage.getItem(`reviewPromptDismissed_${objectId}`)
) { ) {
setShowReviewPrompt(true); setShowReviewPrompt(true);
setShowReviewForm(true);
} }
} catch (error) { } catch (error) {
console.error("Failed to check user review:", error); console.error("Failed to check user review:", error);
} finally { } finally {
setReviewCheckLoading(false); setReviewCheckLoading(false);
} }
}; }, [objectId, userDetails, shop, game]);
const loadReviews = async (reset = false) => { const loadReviews = useCallback(
if (!objectId) return; async (reset = false) => {
if (!objectId) return;
setReviewsLoading(true); if (abortControllerRef.current) {
try { abortControllerRef.current.abort();
const skip = reset ? 0 : reviewsPage * 20;
const response = await window.electron.getGameReviews(
shop,
objectId,
20,
skip,
reviewsSortBy
);
const reviewsData = (response as any)?.reviews || [];
const reviewCount = (response as any)?.totalCount || 0;
if (reset) {
setReviews(reviewsData);
setReviewsPage(0);
setTotalReviewCount(reviewCount);
} else {
setReviews((prev) => [...prev, ...reviewsData]);
} }
setHasMoreReviews(reviewsData.length === 20); const abortController = new AbortController();
} catch (error) { abortControllerRef.current = abortController;
console.error("Failed to load reviews:", error);
} finally { setReviewsLoading(true);
setReviewsLoading(false); try {
} const skip = reset ? 0 : reviewsPage * 20;
}; const response = await window.electron.getGameReviews(
shop,
objectId,
20,
skip,
reviewsSortBy
);
if (abortController.signal.aborted) {
return;
}
const reviewsData = (response as any)?.reviews || [];
const reviewCount = (response as any)?.totalCount || 0;
if (reset) {
setReviews(reviewsData);
setReviewsPage(0);
setTotalReviewCount(reviewCount);
} else {
setReviews((prev) => [...prev, ...reviewsData]);
}
setHasMoreReviews(reviewsData.length === 20);
} catch (error) {
if (!abortController.signal.aborted) {
console.error("Failed to load reviews:", error);
}
} finally {
if (!abortController.signal.aborted) {
setReviewsLoading(false);
}
}
},
[objectId, shop, reviewsPage, reviewsSortBy]
);
const handleVoteReview = async ( const handleVoteReview = async (
reviewId: string, reviewId: string,
@@ -396,7 +439,6 @@ export function GameDetailsContent() {
const handleReviewPromptYes = () => { const handleReviewPromptYes = () => {
setShowReviewPrompt(false); setShowReviewPrompt(false);
setShowReviewForm(true);
setTimeout(() => { setTimeout(() => {
const reviewFormElement = document.querySelector( const reviewFormElement = document.querySelector(
@@ -413,6 +455,7 @@ export function GameDetailsContent() {
const handleReviewPromptLater = () => { const handleReviewPromptLater = () => {
setShowReviewPrompt(false); setShowReviewPrompt(false);
setShowReviewForm(false);
if (objectId) { if (objectId) {
sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true"); sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true");
} }
@@ -451,13 +494,13 @@ export function GameDetailsContent() {
loadReviews(true); loadReviews(true);
checkUserReview(); checkUserReview();
} }
}, [game, shop, objectId, reviewsSortBy, userDetails]); }, [game, shop, objectId, loadReviews, checkUserReview]);
useEffect(() => { useEffect(() => {
if (reviewsPage > 0) { if (reviewsPage > 0) {
loadReviews(false); loadReviews(false);
} }
}, [reviewsPage]); }, [reviewsPage, loadReviews]);
// Initialize previousVotesRef for new reviews // Initialize previousVotesRef for new reviews
useEffect(() => { useEffect(() => {
@@ -773,216 +816,234 @@ export function GameDetailsContent() {
</div> </div>
)} )}
{reviews.map((review) => ( <div
<div key={review.id} className="game-details__review-item"> style={{
{review.isBlocked && opacity: reviewsLoading && reviews.length > 0 ? 0.5 : 1,
!visibleBlockedReviews.has(review.id) ? ( transition: "opacity 0.2s ease",
<div className="game-details__blocked-review-simple"> }}
Review from blocked user {" "} >
<button {reviews.map((review) => (
className="game-details__blocked-review-show-link" <div
onClick={() => toggleBlockedReview(review.id)} key={review.id}
> className="game-details__review-item"
Show >
</button> {review.isBlocked &&
</div> !visibleBlockedReviews.has(review.id) ? (
) : ( <div className="game-details__blocked-review-simple">
<> Review from blocked user {" "}
<div className="game-details__review-header"> <button
<div className="game-details__review-user"> className="game-details__blocked-review-show-link"
{review.user?.profileImageUrl && ( onClick={() => toggleBlockedReview(review.id)}
<button >
className="game-details__review-avatar-button" Show
onClick={() => </button>
review.user?.id && </div>
navigate(`/profile/${review.user.id}`) ) : (
} <>
title={review.user.displayName || "User"} <div className="game-details__review-header">
> <div className="game-details__review-user">
<img {review.user?.profileImageUrl && (
src={review.user.profileImageUrl} <button
alt={review.user.displayName || "User"} className="game-details__review-avatar-button"
className="game-details__review-avatar" onClick={() =>
/> review.user?.id &&
</button> navigate(`/profile/${review.user.id}`)
)} }
<div className="game-details__review-user-info"> title={review.user.displayName || "User"}
<button >
className="game-details__review-display-name game-details__review-display-name--clickable" <img
onClick={() => src={review.user.profileImageUrl}
review.user?.id && alt={review.user.displayName || "User"}
navigate(`/profile/${review.user.id}`) className="game-details__review-avatar"
} />
> </button>
{review.user?.displayName || "Anonymous"} )}
</button> <div className="game-details__review-user-info">
<div className="game-details__review-date"> <button
<ClockIcon size={12} /> className="game-details__review-display-name game-details__review-display-name--clickable"
{formatDistance( onClick={() =>
new Date(review.createdAt), review.user?.id &&
new Date(), navigate(`/profile/${review.user.id}`)
{ addSuffix: true } }
)} >
{review.user?.displayName || "Anonymous"}
</button>
<div className="game-details__review-date">
<ClockIcon size={12} />
{formatDistance(
new Date(review.createdAt),
new Date(),
{ addSuffix: true }
)}
</div>
</div> </div>
</div> </div>
<div
className="game-details__review-score-stars"
title={getRatingText(review.score, t)}
>
{[1, 2, 3, 4, 5].map((starValue) => (
<Star
key={starValue}
size={20}
fill={
starValue <= review.score
? "currentColor"
: "none"
}
className={`game-details__review-star ${
starValue <= review.score
? "game-details__review-star--filled"
: "game-details__review-star--empty"
} ${
starValue <= review.score
? getScoreColorClass(review.score)
: ""
}`}
/>
))}
</div>
</div> </div>
<div <div
className="game-details__review-score-stars" className="game-details__review-content"
title={getRatingText(review.score, t)} dangerouslySetInnerHTML={{
> __html: sanitizeHtml(review.reviewHtml),
{[1, 2, 3, 4, 5].map((starValue) => ( }}
<Star />
key={starValue} <div className="game-details__review-actions">
size={20} <div className="game-details__review-votes">
fill={ <motion.button
starValue <= review.score className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
? "currentColor" onClick={() =>
: "none" handleVoteReview(review.id, "upvote")
}
animate={
review.hasUpvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
} }
className={`game-details__review-star ${
starValue <= review.score
? "game-details__review-star--filled"
: "game-details__review-star--empty"
} ${
starValue <= review.score
? getScoreColorClass(review.score)
: ""
}`}
/>
))}
</div>
</div>
<div
className="game-details__review-content"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(review.reviewHtml),
}}
/>
<div className="game-details__review-actions">
<div className="game-details__review-votes">
<motion.button
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "upvote")
}
animate={
review.hasUpvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsUp size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.upvotes || 0}
custom={
(review.upvotes || 0) >
(previousVotesRef.current.get(review.id)
?.upvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(review.id, {
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
});
}}
>
{formatNumber(review.upvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
<motion.button
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "downvote")
}
animate={
review.hasDownvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsDown size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.downvotes || 0}
custom={
(review.downvotes || 0) >
(previousVotesRef.current.get(review.id)
?.downvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(review.id, {
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
});
}}
>
{formatNumber(review.downvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
</div>
{userDetails?.id === review.user?.id && (
<button
className="game-details__delete-review-button"
onClick={() => handleDeleteReview(review.id)}
title={t("delete_review")}
>
<TrashIcon size={16} />
<span>{t("remove_review")}</span>
</button>
)}
{review.isBlocked &&
visibleBlockedReviews.has(review.id) && (
<button
className="game-details__blocked-review-hide-link"
onClick={() => toggleBlockedReview(review.id)}
> >
Hide <ThumbsUp size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.upvotes || 0}
custom={
(review.upvotes || 0) >
(previousVotesRef.current.get(review.id)
?.upvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(
review.id,
{
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
}
);
}}
>
{formatNumber(review.upvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
<motion.button
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "downvote")
}
animate={
review.hasDownvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsDown size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.downvotes || 0}
custom={
(review.downvotes || 0) >
(previousVotesRef.current.get(review.id)
?.downvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(
review.id,
{
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
}
);
}}
>
{formatNumber(review.downvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
</div>
{userDetails?.id === review.user?.id && (
<button
className="game-details__delete-review-button"
onClick={() => handleDeleteReview(review.id)}
title={t("delete_review")}
>
<TrashIcon size={16} />
<span>{t("remove_review")}</span>
</button> </button>
)} )}
</div> {review.isBlocked &&
</> visibleBlockedReviews.has(review.id) && (
)} <button
</div> className="game-details__blocked-review-hide-link"
))} onClick={() =>
toggleBlockedReview(review.id)
}
>
Hide
</button>
)}
</div>
</>
)}
</div>
))}
</div>
{hasMoreReviews && !reviewsLoading && ( {hasMoreReviews && !reviewsLoading && (
<button <button

View File

@@ -67,6 +67,12 @@ export function EditGameModal({
}; };
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => { const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
// Check if assets were removed (URLs are null but original paths exist)
const iconRemoved = !game.iconUrl && (game as any).originalIconPath;
const logoRemoved = !game.logoImageUrl && (game as any).originalLogoPath;
const heroRemoved =
!game.libraryHeroImageUrl && (game as any).originalHeroPath;
setAssetPaths({ setAssetPaths({
icon: extractLocalPath(game.iconUrl), icon: extractLocalPath(game.iconUrl),
logo: extractLocalPath(game.logoImageUrl), logo: extractLocalPath(game.logoImageUrl),
@@ -85,10 +91,25 @@ export function EditGameModal({
(game as any).originalHeroPath || (game as any).originalHeroPath ||
extractLocalPath(game.libraryHeroImageUrl), extractLocalPath(game.libraryHeroImageUrl),
}); });
// Set removed assets state based on whether assets were explicitly removed
setRemovedAssets({
icon: iconRemoved,
logo: logoRemoved,
hero: heroRemoved,
});
}, []); }, []);
const setNonCustomGameAssets = useCallback( const setNonCustomGameAssets = useCallback(
(game: LibraryGame) => { (game: LibraryGame) => {
// Check if assets were removed (custom URLs are null but original paths exist)
const iconRemoved =
!game.customIconUrl && (game as any).customOriginalIconPath;
const logoRemoved =
!game.customLogoImageUrl && (game as any).customOriginalLogoPath;
const heroRemoved =
!game.customHeroImageUrl && (game as any).customOriginalHeroPath;
setAssetPaths({ setAssetPaths({
icon: extractLocalPath(game.customIconUrl), icon: extractLocalPath(game.customIconUrl),
logo: extractLocalPath(game.customLogoImageUrl), logo: extractLocalPath(game.customLogoImageUrl),
@@ -111,6 +132,13 @@ export function EditGameModal({
extractLocalPath(game.customHeroImageUrl), extractLocalPath(game.customHeroImageUrl),
}); });
// Set removed assets state based on whether assets were explicitly removed
setRemovedAssets({
icon: iconRemoved,
logo: logoRemoved,
hero: heroRemoved,
});
setDefaultUrls({ setDefaultUrls({
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null, icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null, logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
@@ -148,8 +176,12 @@ export function EditGameModal({
}; };
const getAssetDisplayPath = (assetType: AssetType): string => { const getAssetDisplayPath = (assetType: AssetType): string => {
// Use original path if available, otherwise fall back to display path // If asset was removed, don't show any path
return originalAssetPaths[assetType] || assetDisplayPaths[assetType]; if (removedAssets[assetType]) {
return "";
}
// Use display path first, then fall back to original path
return assetDisplayPaths[assetType] || originalAssetPaths[assetType];
}; };
const setAssetPath = (assetType: AssetType, path: string): void => { const setAssetPath = (assetType: AssetType, path: string): void => {
@@ -221,18 +253,11 @@ export function EditGameModal({
}; };
const handleRestoreDefault = (assetType: AssetType) => { const handleRestoreDefault = (assetType: AssetType) => {
if (game && isCustomGame(game)) { // Mark asset as removed and clear paths (for both custom and non-custom games)
// For custom games, mark asset as removed and clear paths setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
setRemovedAssets((prev) => ({ ...prev, [assetType]: true })); setAssetPath(assetType, "");
setAssetPath(assetType, ""); setAssetDisplayPath(assetType, "");
setAssetDisplayPath(assetType, ""); // Don't clear originalAssetPaths - keep them for reference but don't use them for display
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
} else {
// For non-custom games, clear custom assets (restore to shop defaults)
setAssetPath(assetType, "");
setAssetDisplayPath(assetType, "");
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
}
}; };
const getOriginalTitle = (): string => { const getOriginalTitle = (): string => {
@@ -402,10 +427,28 @@ export function EditGameModal({
// Helper function to prepare non-custom game assets // Helper function to prepare non-custom game assets
const prepareNonCustomGameAssets = () => { const prepareNonCustomGameAssets = () => {
const hasIconPath = assetPaths.icon;
let customIconUrl: string | null = null;
if (!removedAssets.icon && hasIconPath) {
customIconUrl = `local:${assetPaths.icon}`;
}
const hasLogoPath = assetPaths.logo;
let customLogoImageUrl: string | null = null;
if (!removedAssets.logo && hasLogoPath) {
customLogoImageUrl = `local:${assetPaths.logo}`;
}
const hasHeroPath = assetPaths.hero;
let customHeroImageUrl: string | null = null;
if (!removedAssets.hero && hasHeroPath) {
customHeroImageUrl = `local:${assetPaths.hero}`;
}
return { return {
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null, customIconUrl,
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null, customLogoImageUrl,
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null, customHeroImageUrl,
}; };
}; };
@@ -439,9 +482,15 @@ export function EditGameModal({
customIconUrl, customIconUrl,
customLogoImageUrl, customLogoImageUrl,
customHeroImageUrl, customHeroImageUrl,
customOriginalIconPath: originalAssetPaths.icon || undefined, customOriginalIconPath: removedAssets.icon
customOriginalLogoPath: originalAssetPaths.logo || undefined, ? undefined
customOriginalHeroPath: originalAssetPaths.hero || undefined, : originalAssetPaths.icon || undefined,
customOriginalLogoPath: removedAssets.logo
? undefined
: originalAssetPaths.logo || undefined,
customOriginalHeroPath: removedAssets.hero
? undefined
: originalAssetPaths.hero || undefined,
}); });
}; };
@@ -484,6 +533,23 @@ export function EditGameModal({
hero: false, hero: false,
}); });
// Clear all asset paths to ensure clean state
setAssetPaths({
icon: "",
logo: "",
hero: "",
});
setAssetDisplayPaths({
icon: "",
logo: "",
hero: "",
});
setOriginalAssetPaths({
icon: "",
logo: "",
hero: "",
});
if (isCustomGame(game)) { if (isCustomGame(game)) {
setCustomGameAssets(game); setCustomGameAssets(game);
// Clear default URLs for custom games // Clear default URLs for custom games