diff --git a/apps/oneclient/desktop/src/api/commands.rs b/apps/oneclient/desktop/src/api/commands.rs index aeb56580..8a0ab057 100644 --- a/apps/oneclient/desktop/src/api/commands.rs +++ b/apps/oneclient/desktop/src/api/commands.rs @@ -53,6 +53,12 @@ pub trait OneClientApi { #[taurpc(alias = "isBundleSyncing")] async fn is_bundle_syncing() -> LauncherResult; + + #[taurpc(alias = "cacheArt")] + async fn cache_art(path: String) -> LauncherResult; + + #[taurpc(alias = "refreshArt")] + async fn refresh_art(path: String) -> LauncherResult<()>; } #[taurpc::ipc_type] @@ -206,4 +212,15 @@ impl OneClientApi for OneClientApiImpl { async fn is_bundle_syncing(self) -> LauncherResult { Ok(crate::oneclient::is_bundle_syncing()) } + + async fn cache_art(self, path: String) -> LauncherResult { + crate::oneclient::clusters::cache_art_image(&path).await + } + + async fn refresh_art(self, path: String) -> LauncherResult<()> { + tokio::spawn(async move { + crate::oneclient::clusters::refresh_art_cache(&path).await; + }); + Ok(()) + } } diff --git a/apps/oneclient/desktop/src/constants.rs b/apps/oneclient/desktop/src/constants.rs index 75cec7c2..b12c79c8 100644 --- a/apps/oneclient/desktop/src/constants.rs +++ b/apps/oneclient/desktop/src/constants.rs @@ -6,4 +6,4 @@ pub const DISCORD_CLIENT_ID: &'static str = "1426999264633946334"; // === Meta === // Cannot contain trailing slash pub const META_URL_BASE: &'static str = - "https://raw.githubusercontent.com/PolyFrost/DataStorage/refs/heads/main/oneclient"; + "https://raw.githubusercontent.com/Polyfrost/DataStorage/refs/heads/main/oneclient"; diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs index bc04f3da..e066ce2b 100644 --- a/apps/oneclient/desktop/src/lib.rs +++ b/apps/oneclient/desktop/src/lib.rs @@ -96,7 +96,9 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> { State::get().await?; - onelauncher_core::api::credentials::refresh_accounts().await?; + if let Err(err) = onelauncher_core::api::credentials::refresh_accounts().await { + tracing::warn!("failed to refresh accounts on startup: {err}"); + } tracing::info!("initialized launcher successfully"); Ok(()) diff --git a/apps/oneclient/desktop/src/oneclient/clusters.rs b/apps/oneclient/desktop/src/oneclient/clusters.rs index 9365a93d..3291aa34 100644 --- a/apps/oneclient/desktop/src/oneclient/clusters.rs +++ b/apps/oneclient/desktop/src/oneclient/clusters.rs @@ -4,7 +4,8 @@ use onelauncher_core::entity::clusters::Model as ClusterModel; use onelauncher_core::entity::loader::GameLoader; use onelauncher_core::error::LauncherResult; use onelauncher_core::send_error; -use onelauncher_core::utils::http::fetch_json; +use onelauncher_core::store::Dirs; +use onelauncher_core::utils::http::{fetch, fetch_json}; use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -61,6 +62,10 @@ pub struct OnlineCluster { #[derive(specta::Type, Deserialize, Serialize)] pub struct OnlineClusterEntry { minor_version: u8, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + art: Option, loader: GameLoader, tags: Vec, } @@ -84,6 +89,50 @@ pub async fn get_data_storage_versions() -> LauncherResult LauncherResult { + let art_dir = Dirs::get_caches_dir().await?.join("oneclient").join("art"); + tokio::fs::create_dir_all(&art_dir) + .await + .map_err(anyhow::Error::from)?; + + // Flatten "/versions/art/Foo.png" → "versions_art_Foo.png" + let filename = path.trim_start_matches('/').replace('/', "_"); + let cached_path = art_dir.join(&filename); + + if cached_path.exists() { + return Ok(cached_path.to_string_lossy().into_owned()); + } + + let url = format!("{}{}", crate::constants::META_URL_BASE, path); + let bytes = fetch(Method::GET, &url).await?; + tokio::fs::write(&cached_path, bytes) + .await + .map_err(anyhow::Error::from)?; + + Ok(cached_path.to_string_lossy().into_owned()) +} + +/// Re-download an art image and overwrite the on-disk cache. +/// Errors are silently ignored — the existing cached version is kept on failure. +/// Call this from `tokio::spawn` so it runs fully in the background. +pub async fn refresh_art_cache(path: &str) { + let Ok(caches_dir) = Dirs::get_caches_dir().await else { + return; + }; + let art_dir = caches_dir.join("oneclient").join("art"); + let filename = path.trim_start_matches('/').replace('/', "_"); + let cached_path = art_dir.join(&filename); + + let url = format!("{}{}", crate::constants::META_URL_BASE, path); + if let Ok(bytes) = fetch(Method::GET, &url).await { + let _ = tokio::fs::write(&cached_path, bytes).await; + } +} + pub async fn init_clusters() -> LauncherResult<()> { let manifest = match fetch_json::( Method::GET, diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index e17febeb..e9b96e9e 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -127,7 +127,7 @@ export type MowojangProfile = { id: string; username: string } export type OnlineCluster = { major_version: number; name: string; art: string; entries: OnlineClusterEntry[] } -export type OnlineClusterEntry = { minor_version: number; loader: GameLoader; tags: string[] } +export type OnlineClusterEntry = { minor_version: number; name?: string | null; art?: string | null; loader: GameLoader; tags: string[] } /** * e.g. @@ -313,8 +313,11 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'core':'{"removeUser":["uuid"],"getClusterById":["id"],"togglePackage":["cluster_id","package_hash"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"],"getClusters":[],"open":["input"],"changeCape":["access_token","cape_uuid"],"refreshAccount":["uuid"],"getLinkedPackages":["cluster_id"],"getUsers":[],"getProfileOrDefault":["name"],"fetchMinecraftProfile":["uuid"],"getLoadersForVersion":["mc_version"],"writeSettings":["setting"],"getDefaultUser":["fallback"],"openMsaLogin":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUser":["uuid"],"removeCape":["access_token"],"createSettingsProfile":["name"],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterById":["id","request"],"getGameVersions":[],"launchCluster":["id","uuid","search_for_java"],"getRunningProcesses":[],"refreshAccounts":[],"getWorlds":["id"],"getScreenshots":["id"],"updateClusterProfile":["name","profile"],"searchPackages":["provider","query"],"setDefaultUser":["uuid"],"readSettings":[],"getPackage":["provider","slug"],"getLogByName":["id","name"],"getMultiplePackages":["provider","slugs"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"syncCluster":["cluster_id"],"isClusterRunning":["cluster_id"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"removePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"killProcess":["pid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"setDiscordRPCMessage":["message"],"removeCluster":["id"],"getProcessLogTail":["id","max_lines"],"changeSkin":["access_token","skin_url","skin_variant"],"createCluster":["options"],"setClusterStage":["id","stage"],"getGlobalProfile":[],"getLogs":["id"],"getPackageBody":["provider","body"],"convertUsernameUUID":["username_uuid"]}', 'debug':'{"openDevTools":[],"isInDev":[],"getPlatform":[],"getPackageVersion":[],"getArch":[],"getType":[],"getLocale":[],"getOsVersion":[],"getFamily":[],"getGitCommitHash":[],"getBuildTimestamp":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"updateBundlePackages":["cluster_id"],"isBundleSyncing":[],"getVersions":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getClustersGroupedByMajor":[],"checkForUpdate":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"installUpdate":[]}' } -export type Router = { 'debug': { openDevTools: () => Promise, +const ARGS_MAP = { 'oneclient':'{"refreshArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"updateBundlePackages":["cluster_id"],"getBundlesFor":["cluster_id"],"getVersions":[],"getClustersGroupedByMajor":[],"checkForUpdate":[],"installUpdate":[],"cacheArt":["path"],"isBundleSyncing":[]}', 'debug':'{"getBuildTimestamp":[],"getPackageVersion":[],"getPlatform":[],"isInDev":[],"getArch":[],"getFamily":[],"openDevTools":[],"getGitCommitHash":[],"getType":[],"getLocale":[],"getOsVersion":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"createSettingsProfile":["name"],"getPackageBody":["provider","body"],"getUsers":[],"launchCluster":["id","uuid","search_for_java"],"getUsersFromAuthor":["provider","author"],"syncCluster":["cluster_id"],"getLinkedPackages":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"getMultiplePackages":["provider","slugs"],"removeCluster":["id"],"togglePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"fetchMinecraftProfile":["uuid"],"getScreenshots":["id"],"updateClusterProfile":["name","profile"],"getRunningProcessesByClusterId":["cluster_id"],"getProfileOrDefault":["name"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProcessLogTail":["id","max_lines"],"getClusterById":["id"],"isClusterRunning":["cluster_id"],"open":["input"],"killProcess":["pid"],"fetchLoggedInProfile":["access_token"],"openMsaLogin":[],"writeSettings":["setting"],"changeCape":["access_token","cape_uuid"],"createCluster":["options"],"setClusterStage":["id","stage"],"searchPackages":["provider","query"],"setDiscordRPCMessage":["message"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removePackage":["cluster_id","package_hash"],"getClusters":[],"getWorlds":["id"],"getDefaultUser":["fallback"],"getPackage":["provider","slug"],"setDefaultUser":["uuid"],"getRunningProcesses":[],"getGlobalProfile":[],"updateClusterById":["id","request"],"refreshAccounts":[],"getGameVersions":[],"getUser":["uuid"],"convertUsernameUUID":["username_uuid"],"refreshAccount":["uuid"],"removeCape":["access_token"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"removeUser":["uuid"],"getLogs":["id"],"getLogByName":["id","name"],"getLoadersForVersion":["mc_version"],"readSettings":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}' } +export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'debug': { openDevTools: () => Promise, isInDev: () => Promise, getArch: () => Promise, getFamily: () => Promise, @@ -333,12 +336,9 @@ checkForUpdate: () => Promise, installUpdate: () => Promise, downloadPackageFromBundle: (package: ModpackFileKind, clusterId: number, bundleName: string, skipCompatibility: boolean | null) => Promise, updateBundlePackages: (clusterId: number) => Promise, -isBundleSyncing: () => Promise }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, +isBundleSyncing: () => Promise, +cacheArt: (path: string) => Promise, +refreshArt: (path: string) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, @@ -392,7 +392,9 @@ changeCape: (accessToken: string, capeUuid: string) => Promise Promise, convertUsernameUUID: (usernameUuid: string) => Promise, setDiscordRPCMessage: (message: string) => Promise, -open: (input: string) => Promise } }; +open: (input: string) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/hooks/useCachedImage.ts b/apps/oneclient/frontend/src/hooks/useCachedImage.ts new file mode 100644 index 00000000..7593e64e --- /dev/null +++ b/apps/oneclient/frontend/src/hooks/useCachedImage.ts @@ -0,0 +1,50 @@ +import { bindings } from '@/main'; +import { convertFileSrc } from '@tauri-apps/api/core'; +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(); + +/** + * Returns the best URL for an art image at the given relative path (e.g. `/versions/art/Foo.png`). + * + * - If the image is already cached on disk, returns the local `asset://` URL immediately. + * - If not cached, downloads it first, then returns the local URL. + * - After the cached version is shown, triggers a one-per-session background refresh that + * overwrites the on-disk cache with any updated version — applied on the next app start. + */ +export function useCachedImage(path: string | null | undefined): string | undefined { + const [src, setSrc] = useState(undefined); + + useEffect(() => { + setSrc(undefined); + if (!path) + return; + + let cancelled = false; + + bindings.oneclient.cacheArt(path) + .then((localPath: string) => { + if (cancelled) + return; + + setSrc(convertFileSrc(localPath)); + + // One background refresh per path per session — no UI update, just overwrites disk + if (!refreshedPaths.has(path)) { + refreshedPaths.add(path); + bindings.oneclient.refreshArt(path).catch(() => {}); + } + }) + .catch(() => { + // Not cached and offline — nothing to show + }); + + return () => { + cancelled = true; + }; + }, [path]); + + return src; +} diff --git a/apps/oneclient/frontend/src/routes/app/clusters.tsx b/apps/oneclient/frontend/src/routes/app/clusters.tsx index 24e5ac1a..6b5fbf92 100644 --- a/apps/oneclient/frontend/src/routes/app/clusters.tsx +++ b/apps/oneclient/frontend/src/routes/app/clusters.tsx @@ -1,5 +1,6 @@ import type { GameLoader } from '@/bindings.gen'; import { GameBackground, LaunchButton, SheetPage } from '@/components'; +import { useCachedImage } from '@/hooks/useCachedImage'; import { bindings } from '@/main'; import useClusterStore from '@/stores/clusterStore'; import { prettifyLoader } from '@/utils/loaders'; @@ -24,6 +25,7 @@ function RouteComponent() { const navigate = Route.useNavigate(); const { data: clusters } = useCommandSuspense(['getClustersGroupedByMajor'], bindings.oneclient.getClustersGroupedByMajor); + const { data: versions } = useCommandSuspense(['getVersions'], () => bindings.oneclient.getVersions()); const loadMinorVersion = useCallback((major: number) => { const existing = prevMinorVersions.find(v => v.major === major); @@ -101,6 +103,23 @@ function RouteComponent() { }); }, [clusters, majorVersion, effectiveMinorVersion, modLoader]); + const selectedMinorArtPath = useMemo(() => { + const onlineCluster = versions.clusters.find(c => c.major_version === majorVersion); + if (!onlineCluster) + return undefined; + + const selectedMinor = parseMcVersion(cluster?.mc_version)?.minor; + if (selectedMinor === undefined) + return onlineCluster.art; + + const loaderMatchedEntry = onlineCluster.entries.find(entry => entry.minor_version === selectedMinor && entry.loader === cluster?.mc_loader); + const minorMatchedEntry = onlineCluster.entries.find(entry => entry.minor_version === selectedMinor); + + return loaderMatchedEntry?.art ?? minorMatchedEntry?.art ?? onlineCluster.art; + }, [versions.clusters, majorVersion, cluster?.mc_version, cluster?.mc_loader]); + + const selectedMinorArtSrc = useCachedImage(selectedMinorArtPath); + const versionInfo = useMemo(() => getVersionInfoOrDefault(cluster?.mc_version), [cluster]); const view = useCallback(() => { @@ -142,13 +161,11 @@ function RouteComponent() { {Object.keys(clusters).map((majorStr) => { const major = Number.parseInt(majorStr, 10); const isSelected = major === majorVersion; - const info = getVersionInfo(major); - - if (!info) - return undefined; + const onlineCluster = versions.clusters.find(c => c.major_version === major); return ( - + {selectedMinorArtSrc + ? ( + {`Minecraft + ) + : ( + + )}

Version {cluster.mc_version}

@@ -177,7 +204,7 @@ function RouteComponent() { setActiveMinorVersion(Number(selected)); setMinorVersion(majorVersion, Number(selected)); }} - selectedKey={effectiveMinorVersion} + selectedKey={effectiveMinorVersion} > {minorVersions.map((minorVersion) => { const fullVer = `${versionInfo.prettyName}.${minorVersion}`; @@ -201,12 +228,12 @@ function RouteComponent() { { - if (!effectiveMinorVersion) - return; + if (!effectiveMinorVersion) + return; - setModLoader(`${majorVersion}.${effectiveMinorVersion}`, selected as GameLoader); - }} - selectedKey={effectiveMinorVersion ? (modLoaders[`${majorVersion}.${effectiveMinorVersion}`] ?? loaders[0]) : loaders[0]} + setModLoader(`${majorVersion}.${effectiveMinorVersion}`, selected as GameLoader); + }} + selectedKey={effectiveMinorVersion ? (modLoaders[`${majorVersion}.${effectiveMinorVersion}`] ?? loaders[0]) : loaders[0]} > {loaders.map(loader => ( unknown; tags: Array; }) { + const artSrc = useCachedImage(artPath); const info = getVersionInfo(major_version); if (!info) @@ -276,7 +306,17 @@ function ClusterEntry({ return (
- + {artSrc + ? ( + {`Minecraft + ) + : ( + + )}
bindings.oneclient.getVersions()); const { data: lastPlayedClusters } = useLastPlayedClusters(); const setActiveClusterId = useAppShellStore(state => state.setActiveClusterId); @@ -34,7 +37,7 @@ function RouteComponent() { return (
- + {lastPlayedClusters.slice(0, 3).map(cluster => ( @@ -44,6 +47,7 @@ function RouteComponent() { loader={cluster.mc_loader} onPress={() => setActiveClusterId(cluster.id)} version={cluster.mc_version} + versions={versions} /> ))} @@ -59,8 +63,9 @@ function RouteComponent() { ); } -function ActiveClusterInfo({ cluster }: { cluster: ClusterModel }) { +function ActiveClusterInfo({ cluster, versions }: { cluster: ClusterModel; versions: OnlineClusterManifest }) { const versionInfo = getVersionInfoOrDefault(cluster.mc_version); + const entry = getOnlineEntryForVersion(cluster.mc_version, versions); const navigate = useNavigate({ from: Route.id }); const viewCluster = () => { @@ -72,6 +77,8 @@ function ActiveClusterInfo({ cluster }: { cluster: ClusterModel }) { }); }; + const subtitle = entry?.name ?? versionInfo.shortDescription; + return (

{cluster.mc_version} {prettifyLoader(cluster.mc_loader)}

-

{versionInfo.shortDescription}

+

{subtitle}

@@ -105,6 +112,7 @@ interface RecentsCardProps { loader: GameLoader; onPress: () => void; active: boolean; + versions: OnlineClusterManifest; } function RecentsCard({ @@ -112,10 +120,16 @@ function RecentsCard({ loader, onPress, active, + versions, }: RecentsCardProps) { const versionInfo = getVersionInfo(version); + const entry = getOnlineEntryForVersion(version, versions); + const onlineCluster = getOnlineClusterForVersion(version, versions); - if (!versionInfo) + const artPath = entry?.art ?? onlineCluster?.art; + const artSrc = useCachedImage(artPath); + + if (!versionInfo && !artPath) return (

Unknown Version

@@ -125,7 +139,17 @@ function RecentsCard({ return (
- + {artSrc + ? ( + {`Minecraft + ) + : ( + + )}
bindings.oneclient.getVersions()); + + const entry = getOnlineEntryForVersion(cluster.mc_version, versions); + const onlineCluster = getOnlineClusterForVersion(cluster.mc_version, versions); + const artPath = entry?.art ?? onlineCluster?.art; + const artSrc = useCachedImage(artPath); return (
@@ -97,10 +106,20 @@ function BackgroundGradient() { }} key={(cluster.mc_version) + (cluster.mc_loader)} > - + {artSrc + ? ( + + ) + : ( + + )}
diff --git a/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx b/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx index 28c771bb..3ae6deda 100644 --- a/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx +++ b/apps/oneclient/frontend/src/routes/onboarding/preferences/version.tsx @@ -1,5 +1,6 @@ import type { GameLoader, OnlineCluster, OnlineClusterEntry } from '@/bindings.gen'; import type { VersionInfo } from '@/utils/versionMap'; +import { useCachedImage } from '@/hooks/useCachedImage'; import { bindings } from '@/main'; import { OnboardingNavigation } from '@/routes/onboarding/route'; import { getVersionInfoOrDefault } from '@/utils/versionMap'; @@ -80,13 +81,16 @@ function VersionCard({ cluster, versionData, version, fullVersionName, setSelect }); }; + const artPath = version.art ?? cluster.art; + const artSrc = useCachedImage(artPath); + return (
{`Minecraft
diff --git a/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx b/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx index 6c009557..d7bb0982 100644 --- a/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx +++ b/apps/oneclient/frontend/src/routes/onboarding/preferences/versionCategory.tsx @@ -2,6 +2,7 @@ import type { ClusterModel, ModpackArchive, ModpackFile } from '@/bindings.gen'; import type { DownloadModsRef, ModCardContextApi, ModWithBundle, onClickOnMod } from '@/components'; import type { StrippedCluster } from '@/routes/onboarding/preferences/version'; import { BundleModListModal, DownloadMods, ModCardContext, Overlay } from '@/components'; +import { useCachedImage } from '@/hooks/useCachedImage'; import { bindings } from '@/main'; import { OnboardingNavigation } from '@/routes/onboarding/route'; import { useCommandSuspense } from '@onelauncher/common'; @@ -40,10 +41,14 @@ function RouteComponent() { const selected = selectedClusters.some(selectedCluster => selectedCluster.mc_version === cluster.mc_version && selectedCluster.mc_loader === cluster.mc_loader); if (!selected) return; - const version = versions.clusters.find(version => cluster.mc_version.startsWith(`1.${version.major_version}`)); + const onlineCluster = versions.clusters.find(c => cluster.mc_version.startsWith(`1.${c.major_version}`)); + const minorVersion = Number.parseInt(cluster.mc_version.split('.')[2] ?? '', 10); + const onlineEntry = !Number.isNaN(minorVersion) + ? onlineCluster?.entries.find(e => e.minor_version === minorVersion) + : undefined; bundlesData[cluster.name] = { bundles: bundleQueries[i], - art: version?.art ?? '/versions/art/Horse_Update.jpg', + art: onlineEntry?.art ?? onlineCluster?.art ?? '/versions/art/Horse_Update.jpg', modsInfo: [], clusterId: cluster.id, }; @@ -148,7 +153,8 @@ function ModCategory({ bundleData, name, modsPerCluster, setModsPerCluster }: { ); } -function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterId }: { fullVersionName: string; art: string; bundle: ModpackArchive; mods: Array; setMods: React.Dispatch>>; clusterId: number }) { +function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterId }: { fullVersionName: string; art: string | null | undefined; bundle: ModpackArchive; mods: Array; setMods: React.Dispatch>>; clusterId: number }) { + const artSrc = useCachedImage(art); const files = bundle.manifest.files; const enabledFiles = files.filter(file => file.enabled); const hiddenEnabledFiles = enabledFiles.filter(file => file.hidden); @@ -210,7 +216,7 @@ function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterI {`Minecraft
diff --git a/apps/oneclient/frontend/src/utils/versionMap.ts b/apps/oneclient/frontend/src/utils/versionMap.ts index ecf5b963..be3dbd6d 100644 --- a/apps/oneclient/frontend/src/utils/versionMap.ts +++ b/apps/oneclient/frontend/src/utils/versionMap.ts @@ -1,3 +1,4 @@ +import type { OnlineCluster, OnlineClusterEntry, OnlineClusterManifest } from '@/bindings.gen'; import type { GameBackgroundName } from '@/components'; export interface VersionInfo { @@ -54,6 +55,29 @@ export function getVersionInfoOrDefault(version: number | string | null | undefi return info ?? VERSION_MAP[-1]; } +export function getOnlineClusterForVersion( + version: string | null | undefined, + versions: OnlineClusterManifest, +): OnlineCluster | undefined { + const parsed = parseMcVersion(version); + if (!parsed) + return undefined; + return versions.clusters.find(c => c.major_version === parsed.major); +} + +export function getOnlineEntryForVersion( + version: string | null | undefined, + versions: OnlineClusterManifest, +): OnlineClusterEntry | undefined { + const parsed = parseMcVersion(version); + if (!parsed || parsed.minor === undefined) + return undefined; + const cluster = versions.clusters.find(c => c.major_version === parsed.major); + if (!cluster) + return undefined; + return cluster.entries.find(e => e.minor_version === parsed.minor); +} + export interface ParsedMcVersion { major: number; minor: number | undefined; diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index f45ad4e5..b538b94a 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -4,7 +4,7 @@ * @type {import('lint-staged').Configuration} */ const config = { - '*.ts': 'eslint', + '*.ts': 'eslint --no-warn-ignored', '*.{js,mjs,cjs}': 'eslint', // handle JS, MJS, CJS in one fell swoop '*.rs': [ 'rustfmt', // use rustfmt to only check the files provided by lint-staged