Add support for resource packs + shaders + datapacks

This commit is contained in:
Wyvest 2026-02-23 06:30:01 -05:00
parent 3bcf55907c
commit 6a2352c5dd
9 changed files with 175 additions and 27 deletions

View file

@ -303,15 +303,10 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'debug':'{"getArch":[],"openDevTools":[],"getType":[],"getPackageVersion":[],"getGitCommitHash":[],"getFamily":[],"getPlatform":[],"getLocale":[],"getOsVersion":[],"isInDev":[],"getBuildTimestamp":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"checkForUpdate":[],"extractBundleOverrides":["bundle_path","cluster_id"],"installUpdate":[],"getBundlesFor":["cluster_id"],"getVersions":[]}', 'core':'{"getRunningProcesses":[],"installModpack":["modpack","cluster_id"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"setDefaultUser":["uuid"],"killProcess":["pid"],"getWorlds":["id"],"createSettingsProfile":["name"],"removeCape":["access_token"],"getScreenshots":["id"],"getLoadersForVersion":["mc_version"],"getLogs":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"refreshAccount":["uuid"],"getGameVersions":[],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getUser":["uuid"],"isClusterRunning":["cluster_id"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getClusterById":["id"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"convertUsernameUUID":["username_uuid"],"openMsaLogin":[],"createCluster":["options"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"getUsersFromAuthor":["provider","author"],"getClusters":[],"getPackageBody":["provider","body"],"removePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"open":["input"],"refreshAccounts":[],"writeSettings":["setting"],"searchPackages":["provider","query"],"getMultiplePackages":["provider","slugs"],"getDefaultUser":["fallback"],"getUsers":[],"getLinkedPackages":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"setClusterStage":["id","stage"],"fetchLoggedInProfile":["access_token"],"setDiscordRPCMessage":["message"],"removeUser":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"]}' }
export type Router = { 'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
checkForUpdate: () => Promise<Update | null>,
installUpdate: () => Promise<null> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
const ARGS_MAP = { 'oneclient':'{"getClustersGroupedByMajor":[],"checkForUpdate":[],"extractBundleOverrides":["bundle_path","cluster_id"],"installUpdate":[],"getBundlesFor":["cluster_id"],"getVersions":[]}', 'core':'{"getRunningProcesses":[],"installModpack":["modpack","cluster_id"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"setDefaultUser":["uuid"],"killProcess":["pid"],"getWorlds":["id"],"createSettingsProfile":["name"],"removeCape":["access_token"],"getScreenshots":["id"],"getLoadersForVersion":["mc_version"],"getLogs":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"refreshAccount":["uuid"],"getGameVersions":[],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getUser":["uuid"],"isClusterRunning":["cluster_id"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getClusterById":["id"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"convertUsernameUUID":["username_uuid"],"openMsaLogin":[],"createCluster":["options"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"getUsersFromAuthor":["provider","author"],"getClusters":[],"getPackageBody":["provider","body"],"removePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"open":["input"],"refreshAccounts":[],"writeSettings":["setting"],"searchPackages":["provider","query"],"getMultiplePackages":["provider","slugs"],"getDefaultUser":["fallback"],"getUsers":[],"getLinkedPackages":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"setClusterStage":["id","stage"],"fetchLoggedInProfile":["access_token"],"setDiscordRPCMessage":["message"],"removeUser":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getArch":[],"openDevTools":[],"getType":[],"getPackageVersion":[],"getGitCommitHash":[],"getFamily":[],"getPlatform":[],"getLocale":[],"getOsVersion":[],"isInDev":[],"getBuildTimestamp":[]}' }
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
removeCluster: (id: number) => Promise<null>,
@ -363,9 +358,14 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
checkForUpdate: () => Promise<Update | null>,
installUpdate: () => Promise<null> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,

View file

@ -93,14 +93,11 @@ export interface ModCardContextApi {
installedPackages?: Array<PackageModel>;
}
const DEFAULT_MOD_CARD_CONTEXT: ModCardContextApi = {};
export const ModCardContext = createContext<ModCardContextApi | null>(null);
export function useModCardContext() {
const ctx = useContext(ModCardContext);
if (!ctx)
throw new Error('useModCardContext must be used within a ModCardContext.Provider');
return ctx;
return useContext(ModCardContext) ?? DEFAULT_MOD_CARD_CONTEXT;
}
export function ModCard({ file, cluster }: ModCardProps) {

View file

@ -1,7 +1,8 @@
import type { ClusterModel, ModpackArchive } from '@/bindings.gen';
import { Bundle, useModCardContext } from '@/components';
import { Bundle, ModCardContext } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { Button, Tab, TabContent, TabList, TabPanel, Tabs } from '@onelauncher/common/components';
import { useContext } from 'react';
function getBundleName(name: string): string {
return (name.match(/\[(.*?)\]/)?.[1]) ?? 'Loading...';
@ -15,15 +16,18 @@ interface ModListProps {
}
export function ModList({ bundles, cluster, selectedTab, onTabChange }: ModListProps) {
const { useVerticalGridLayout } = useModCardContext();
const ctx = useContext(ModCardContext);
const useVerticalGridLayout = ctx?.useVerticalGridLayout ?? false;
const { createSetting } = useSettings();
const [useGridLayout, setUseGrid] = createSetting('mod_list_use_grid');
const nonEmptyBundles = bundles.filter(b => b.manifest.files.length > 0);
return (
<Tabs defaultValue={selectedTab ?? getBundleName(bundles[0].manifest.name)} onTabChange={onTabChange}>
<Tabs defaultValue={selectedTab ?? getBundleName(nonEmptyBundles[0]?.manifest.name ?? bundles[0].manifest.name)} onTabChange={onTabChange}>
<TabList className="justify-between px-4 py-3" height={false}>
<div className="flex flex-row gap-6 px-2">
{bundles.map(bundle => <Tab key={bundle.path} value={getBundleName(bundle.manifest.name)}>{getBundleName(bundle.manifest.name)}</Tab>)}
{nonEmptyBundles.map(bundle => <Tab key={bundle.path} value={getBundleName(bundle.manifest.name)}>{getBundleName(bundle.manifest.name)}</Tab>)}
</div>
{!useVerticalGridLayout && (
<Button onPress={() => setUseGrid(!useGridLayout)}>
@ -33,7 +37,7 @@ export function ModList({ bundles, cluster, selectedTab, onTabChange }: ModListP
</TabList>
<TabContent className={useVerticalGridLayout ? 'pt-0' : ''}>
{bundles.map(bundleData => (
{nonEmptyBundles.map(bundleData => (
<TabPanel className={useVerticalGridLayout ? 'max-w-192' : ''} key={getBundleName(bundleData.manifest.name)} value={getBundleName(bundleData.manifest.name)}>
<Bundle cluster={cluster} files={bundleData.manifest.files} />
</TabPanel>

View file

@ -40,6 +40,9 @@ const ResolvedPathNames: Record<URLPath, string> = {
'/app/cluster/browser/package': 'Browsing {packageName}',
'/app/cluster/logs': 'Viewing {clusterName}\'s logs',
'/app/cluster/mods': 'Viewing {clusterName}\'s mods',
'/app/cluster/resource-packs': 'Viewing {clusterName}\'s resource packs',
'/app/cluster/shaders': 'Viewing {clusterName}\'s shaders',
'/app/cluster/datapacks': 'Viewing {clusterName}\'s data packs',
'/app/cluster/process': 'Viewing {clusterName}',
'/app/cluster/settings': 'Viewing {clusterName}\'s settings',
'/app/settings': 'Viewing Settings',
@ -71,7 +74,9 @@ function useDiscordRPC() {
const { data: cluster } = useCommand(['getClusterById', clusterId], () => bindings.core.getClusterById(clusterId));
const { data: managedPackage } = useCommand(['getPackage', provider, packageId], () => bindings.core.getPackage(provider!, packageId!), { enabled: provider != null && packageId != null });
useEffect(() => {
bindings.core.setDiscordRPCMessage(ReplaceVariables(ResolvedPathNames[location.pathname as URLPath], { clusterName: cluster?.name ?? 'UNKNOWN', packageName: managedPackage?.name ?? 'UNKNOWN' }));
const template = ResolvedPathNames[location.pathname as URLPath];
if (template)
bindings.core.setDiscordRPCMessage(ReplaceVariables(template, { clusterName: cluster?.name ?? 'UNKNOWN', packageName: managedPackage?.name ?? 'UNKNOWN' }));
}, [location.pathname, location.search.clusterId, cluster?.name, managedPackage?.name]);
}

View file

@ -0,0 +1,42 @@
import type { ModCardContextApi } from '@/components';
import { ModCardContext, ModList } from '@/components';
import { getFilePackageType } from '@/routes/app/cluster/mods';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { createFileRoute } from '@tanstack/react-router';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useMemo } from 'react';
export const Route = createFileRoute('/app/cluster/datapacks')({
component: RouteComponent,
});
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { data: bundles } = useCommandSuspense(['getBundlesFor', cluster.id], () => bindings.oneclient.getBundlesFor(cluster.id));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const filteredBundles = useMemo(() =>
bundles.map(b => ({
...b,
manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'datapack') },
})), [bundles]);
const context = useMemo<ModCardContextApi>(() => ({
enableClickToDownload: true,
installedPackages,
}), [installedPackages]);
if (filteredBundles.every(b => b.manifest.files.length === 0))
return <p className="p-4 text-fg-secondary">No data packs found in {cluster.name}</p>;
return (
<OverlayScrollbarsComponent className="bg-none">
<div className="min-h-148">
<ModCardContext.Provider value={context}>
<ModList bundles={filteredBundles} cluster={cluster} />
</ModCardContext.Provider>
</div>
</OverlayScrollbarsComponent>
);
}

View file

@ -1,3 +1,4 @@
import type { ModpackFile } from '@/bindings.gen';
import type { ModCardContextApi } from '@/components';
import { ModCardContext, ModList } from '@/components';
import { bindings } from '@/main';
@ -10,24 +11,36 @@ export const Route = createFileRoute('/app/cluster/mods')({
component: RouteComponent,
});
export function getFilePackageType(file: ModpackFile) {
if ('Managed' in file.kind)
return file.kind.Managed[0].package_type;
return file.kind.External.package_type;
}
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { data: bundles } = useCommandSuspense(['getBundlesFor', cluster.id], () => bindings.oneclient.getBundlesFor(cluster.id));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const filteredBundles = useMemo(() =>
bundles.map(b => ({
...b,
manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'mod') },
})), [bundles]);
const context = useMemo<ModCardContextApi>(() => ({
enableClickToDownload: true,
installedPackages,
}), [installedPackages]);
if (bundles.length === 0)
return <p>No bundles found {cluster.name}</p>;
if (filteredBundles.every(b => b.manifest.files.length === 0))
return <p className="p-4 text-fg-secondary">No mods found in {cluster.name}</p>;
return (
<OverlayScrollbarsComponent className="bg-none">
<div className="min-h-148">
<ModCardContext.Provider value={context}>
<ModList bundles={bundles} cluster={cluster} />
<ModList bundles={filteredBundles} cluster={cluster} />
</ModCardContext.Provider>
</div>
</OverlayScrollbarsComponent>

View file

@ -0,0 +1,42 @@
import type { ModCardContextApi } from '@/components';
import { ModCardContext, ModList } from '@/components';
import { getFilePackageType } from '@/routes/app/cluster/mods';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { createFileRoute } from '@tanstack/react-router';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useMemo } from 'react';
export const Route = createFileRoute('/app/cluster/resource-packs')({
component: RouteComponent,
});
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { data: bundles } = useCommandSuspense(['getBundlesFor', cluster.id], () => bindings.oneclient.getBundlesFor(cluster.id));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const filteredBundles = useMemo(() =>
bundles.map(b => ({
...b,
manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'resourcepack') },
})), [bundles]);
const context = useMemo<ModCardContextApi>(() => ({
enableClickToDownload: true,
installedPackages,
}), [installedPackages]);
if (filteredBundles.every(b => b.manifest.files.length === 0))
return <p className="p-4 text-fg-secondary">No resource packs found in {cluster.name}</p>;
return (
<OverlayScrollbarsComponent className="bg-none">
<div className="min-h-148">
<ModCardContext.Provider value={context}>
<ModList bundles={filteredBundles} cluster={cluster} />
</ModCardContext.Provider>
</div>
</OverlayScrollbarsComponent>
);
}

View file

@ -79,6 +79,9 @@ function RouteComponent() {
>
<TabList floating={floating} ref={tabListRef}>
<Tab from={Route.id} search={search} to="/app/cluster/mods">Mods</Tab>
<Tab from={Route.id} search={search} to="/app/cluster/resource-packs">Resource Packs</Tab>
<Tab from={Route.id} search={search} to="/app/cluster/shaders">Shaders</Tab>
<Tab from={Route.id} search={search} to="/app/cluster/datapacks">Data Packs</Tab>
<Tab from={Route.id} search={search} to="/app/cluster/logs">Logs</Tab>
<Tab from={Route.id} search={{ ...search, packageType: 'mod' }} to="/app/cluster/browser">Browser</Tab>
<Tab from={Route.id} search={search} to="/app/cluster/settings">Settings</Tab>

View file

@ -0,0 +1,42 @@
import type { ModCardContextApi } from '@/components';
import { ModCardContext, ModList } from '@/components';
import { getFilePackageType } from '@/routes/app/cluster/mods';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { createFileRoute } from '@tanstack/react-router';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useMemo } from 'react';
export const Route = createFileRoute('/app/cluster/shaders')({
component: RouteComponent,
});
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { data: bundles } = useCommandSuspense(['getBundlesFor', cluster.id], () => bindings.oneclient.getBundlesFor(cluster.id));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const filteredBundles = useMemo(() =>
bundles.map(b => ({
...b,
manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'shader') },
})), [bundles]);
const context = useMemo<ModCardContextApi>(() => ({
enableClickToDownload: true,
installedPackages,
}), [installedPackages]);
if (filteredBundles.every(b => b.manifest.files.length === 0))
return <p className="p-4 text-fg-secondary">No shaders found in {cluster.name}</p>;
return (
<OverlayScrollbarsComponent className="bg-none">
<div className="min-h-148">
<ModCardContext.Provider value={context}>
<ModList bundles={filteredBundles} cluster={cluster} />
</ModCardContext.Provider>
</div>
</OverlayScrollbarsComponent>
);
}