diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 18b0d442..bef6499b 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -250,7 +250,7 @@ export type SettingProfileModel = { name: string; java_id: number | null; res: R export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; seen_onboarding: boolean; mod_list_use_grid: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean; log_debug_info: boolean; show_tanstack_dev_tools: boolean } -export type SettingsOsExtra = { enable_gamemode: boolean | null } +export type SettingsOsExtra = Record export type SkinVariant = "classic" | "slim" @@ -317,12 +317,10 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'debug':'{"getFullDebugInfoParsedString":[],"getBuildTimestamp":[],"getPackageVersion":[],"openDevTools":[],"getFamily":[],"getArch":[],"isInDev":[],"getFullDebugInfo":[],"getType":[],"getLocale":[],"getPlatform":[],"getOsDistro":[],"getGitCommitHash":[],"getOsVersion":[],"getFullDebugInfoParsed":[]}', 'oneclient':'{"updateBundlePackages":["cluster_id"],"installUpdate":[],"checkForUpdate":[],"cacheArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"],"isBundleSyncing":[],"getBundlesFor":["cluster_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getVersions":[],"getClustersGroupedByMajor":[],"refreshArt":["path"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"removeCape":["access_token"],"changeSkin":["access_token","skin_url","skin_variant"],"getLinkedPackages":["cluster_id"],"fetchMinecraftProfile":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"updateClusterById":["id","request"],"getLogs":["id"],"openMsaLogin":[],"getGameVersions":[],"setDefaultUser":["uuid"],"syncCluster":["cluster_id"],"refreshAccount":["uuid"],"removeCluster":["id"],"removePackage":["cluster_id","package_hash"],"convertUsernameUUID":["username_uuid"],"getClusterById":["id"],"fetchLoggedInProfile":["access_token"],"removeUser":["uuid"],"getLogByName":["id","name"],"launchCluster":["id","uuid","search_for_java"],"readSettings":[],"createCluster":["options"],"getGlobalProfile":[],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getRunningProcessesByClusterId":["cluster_id"],"refreshAccounts":[],"getDefaultUser":["fallback"],"getUsers":[],"getPackage":["provider","slug"],"getPackageBody":["provider","body"],"getClusters":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"setDiscordRPCMessage":["message"],"updateClusterProfile":["name","profile"],"setClusterStage":["id","stage"],"getUser":["uuid"],"killProcess":["pid"],"getWorlds":["id"],"installModpack":["modpack","cluster_id"],"open":["input"],"createSettingsProfile":["name"],"getProcessLogTail":["id","max_lines"],"getMultiplePackages":["provider","slugs"],"getRunningProcesses":[],"getLoadersForVersion":["mc_version"],"getProfileOrDefault":["name"],"writeSettings":["setting"],"getScreenshots":["id"],"getUsersFromAuthor":["provider","author"],"togglePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"]}' } +const ARGS_MAP = { 'debug':'{"openDevTools":[],"getLocale":[],"getFullDebugInfoParsedString":[],"getArch":[],"getPlatform":[],"getOsVersion":[],"getOsDistro":[],"isInDev":[],"getFullDebugInfoParsed":[],"getFamily":[],"getGitCommitHash":[],"getType":[],"getPackageVersion":[],"getFullDebugInfo":[],"getBuildTimestamp":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"launchCluster":["id","uuid","search_for_java"],"getRunningProcessesByClusterId":["cluster_id"],"getDefaultUser":["fallback"],"refreshAccounts":[],"getPackage":["provider","slug"],"searchPackages":["provider","query"],"getProcessLogTail":["id","max_lines"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"togglePackage":["cluster_id","package_hash"],"fetchLoggedInProfile":["access_token"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"changeCape":["access_token","cape_uuid"],"fetchMinecraftProfile":["uuid"],"setClusterStage":["id","stage"],"convertUsernameUUID":["username_uuid"],"getGlobalProfile":[],"writeSettings":["setting"],"getScreenshots":["id"],"removeUser":["uuid"],"setDiscordRPCMessage":["message"],"isClusterRunning":["cluster_id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"installModpack":["modpack","cluster_id"],"getRunningProcesses":[],"syncCluster":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCape":["access_token"],"getLoadersForVersion":["mc_version"],"open":["input"],"getLinkedPackages":["cluster_id"],"getWorlds":["id"],"getLogs":["id"],"createCluster":["options"],"getLogByName":["id","name"],"getClusters":[],"getProfileOrDefault":["name"],"getUser":["uuid"],"createSettingsProfile":["name"],"openMsaLogin":[],"readSettings":[],"getPackageBody":["provider","body"],"removeCluster":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getClusterById":["id"],"updateClusterById":["id","request"],"killProcess":["pid"],"setDefaultUser":["uuid"],"removePackage":["cluster_id","package_hash"],"getUsers":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"refreshAccount":["uuid"]}', 'oneclient':'{"cacheArt":["path"],"getClustersGroupedByMajor":[],"refreshArt":["path"],"installUpdate":[],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"updateBundlePackages":["cluster_id"],"getBundlesFor":["cluster_id"],"isBundleSyncing":[],"getVersions":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"]}' } export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, 'debug': { openDevTools: () => Promise, isInDev: () => Promise, getArch: () => Promise, @@ -338,6 +336,8 @@ getPackageVersion: () => Promise, getFullDebugInfo: () => Promise, getFullDebugInfoParsed: () => Promise, getFullDebugInfoParsedString: () => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, diff --git a/apps/oneclient/frontend/src/hooks/useCachedImage.ts b/apps/oneclient/frontend/src/hooks/useCachedImage.ts index 7593e64e..53cfcbd2 100644 --- a/apps/oneclient/frontend/src/hooks/useCachedImage.ts +++ b/apps/oneclient/frontend/src/hooks/useCachedImage.ts @@ -5,6 +5,52 @@ import { useEffect, useState } from 'react'; // Track which paths have already been refreshed this session so we only // hit the network once per path, not on every component mount. const refreshedPaths: Set = new Set(); +const prefetchedPaths: Set = new Set(); + +function normalizePaths(paths: Array): Array { + return Array.from(new Set(paths.filter((path): path is string => Boolean(path)))); +} + +/** + * Eagerly caches a list of art paths with bounded concurrency. + * This is intended for onboarding prefetch so version cards can render + * immediately from disk cache instead of waiting for per-card fetches. + */ +export async function prefetchCachedImages( + paths: Array, + options?: { concurrency?: number }, +): Promise { + const normalizedPaths = normalizePaths(paths).filter(path => !prefetchedPaths.has(path)); + if (normalizedPaths.length === 0) + return; + + const concurrency = Math.max(1, options?.concurrency ?? 4); + let nextIndex = 0; + + const worker = async () => { + while (nextIndex < normalizedPaths.length) { + const index = nextIndex++; + const path = normalizedPaths[index]; + if (!path) + continue; + + prefetchedPaths.add(path); + try { + await bindings.oneclient.cacheArt(path); + if (!refreshedPaths.has(path)) { + refreshedPaths.add(path); + bindings.oneclient.refreshArt(path).catch(() => {}); + } + } + catch { + // Keep failures non-fatal and allow retry in a future prefetch call. + prefetchedPaths.delete(path); + } + } + }; + + await Promise.all(Array.from({ length: Math.min(concurrency, normalizedPaths.length) }, worker)); +} /** * Returns the best URL for an art image at the given relative path (e.g. `/versions/art/Foo.png`). diff --git a/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx b/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx index 5dbe0a22..aeab6859 100644 --- a/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx +++ b/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx @@ -87,11 +87,17 @@ function VersionCard({ cluster, versionData: _versionData, version, fullVersionN return (
- {`Minecraft + {artSrc + ? ( + {`Minecraft + ) + : ( +
+ )}
{ diff --git a/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx b/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx index 936d926e..4c7b4a73 100644 --- a/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx +++ b/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx @@ -220,11 +220,17 @@ function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterI return (
- {`Minecraft + {artSrc + ? ( + {`Minecraft + ) + : ( +
+ )}
diff --git a/apps/oneclient/frontend/src/routes/onboarding/route.tsx b/apps/oneclient/frontend/src/routes/onboarding/route.tsx index 25244aea..f9578a52 100644 --- a/apps/oneclient/frontend/src/routes/onboarding/route.tsx +++ b/apps/oneclient/frontend/src/routes/onboarding/route.tsx @@ -2,6 +2,7 @@ import type { DownloadModsRef } from '@/components'; import type { PropsWithChildren } from 'react'; import LauncherLogo from '@/assets/logos/oneclient.svg?react'; import { GameBackground, LoaderSuspense, MadeBy, NavbarButton, Overlay, Stepper, SuperSecretDevOptions } from '@/components'; +import { prefetchCachedImages } from '@/hooks/useCachedImage'; import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; @@ -105,6 +106,7 @@ function RouteComponent() { // Prefetch data so that onboarding/preferences/versionCategory is fast const queryClient = useQueryClient(); const { data: clusters } = useCommandSuspense(['getClusters'], () => bindings.core.getClusters()); + const { data: versions } = useCommandSuspense(['getVersions'], () => bindings.oneclient.getVersions()); useEffect(() => { clusters.forEach((cluster) => { queryClient.prefetchQuery({ @@ -113,6 +115,16 @@ function RouteComponent() { }); }); }, [clusters, queryClient]); + useEffect(() => { + const artPaths: Array = ['/versions/art/Horse_Update.jpg']; + for (const cluster of versions.clusters) { + artPaths.push(cluster.art); + for (const entry of cluster.entries) + artPaths.push(entry.art); + } + + void prefetchCachedImages(artPaths); + }, [versions.clusters]); const { currentLinearStepIndex } = Route.useLoaderData();