feat: cluster settings

This commit is contained in:
Szedann 2025-09-30 20:35:33 +02:00
parent 1eb6505c0c
commit 3d0658a950
5 changed files with 184 additions and 7 deletions

View file

@ -10,7 +10,7 @@ export type ClusterModel = { id: number; folder_name: string; stage: ClusterStag
export type ClusterStage = "notready" | "downloading" | "repairing" | "ready"
export type ClusterUpdate = { name: string | null; icon_url: Icon | null }
export type ClusterUpdate = { name: string | null; icon_url: Icon | null; setting_profile_name: string | null }
export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null }
@ -181,7 +181,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; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean }
export type SettingsOsExtra = { enable_gamemode: boolean | null }
export type SettingsOsExtra = Record<string, never>
export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated"
@ -244,7 +244,7 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'core':'{"open":["input"],"setDefaultUser":["uuid"],"getScreenshots":["id"],"getClusters":[],"getClusterById":["id"],"getDefaultUser":["fallback"],"removeCluster":["id"],"getProfileOrDefault":["name"],"getRunningProcessesByClusterId":["cluster_id"],"getWorlds":["id"],"fetchMinecraftProfile":["uuid"],"getUser":["uuid"],"getLogByName":["id","name"],"writeSettings":["setting"],"installModpack":["modpack","cluster_id"],"killProcess":["pid"],"updateClusterProfile":["name","profile"],"getUsersFromAuthor":["provider","author"],"getUsers":[],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getPackage":["provider","slug"],"updateClusterById":["id","request"],"getLogs":["id"],"getGameVersions":[],"getGlobalProfile":[],"searchPackages":["provider","query"],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"getMultiplePackages":["provider","slugs"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"removeUser":["uuid"],"createCluster":["options"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}' }
const ARGS_MAP = { 'oneclient':'{"getBundlesFor":["cluster_id"],"getClustersGroupedByMajor":[],"openDevTools":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'core':'{"updateClusterProfile":["name","profile"],"getPackageBody":["provider","body"],"killProcess":["pid"],"getWorlds":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getLoadersForVersion":["mc_version"],"createSettingsProfile":["name"],"getProfileOrDefault":["name"],"getGlobalProfile":[],"updateClusterById":["id","request"],"getUsers":[],"getRunningProcesses":[],"fetchMinecraftProfile":["uuid"],"getScreenshots":["id"],"open":["input"],"isClusterRunning":["cluster_id"],"installModpack":["modpack","cluster_id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"searchPackages":["provider","query"],"getUser":["uuid"],"getLogByName":["id","name"],"removeCluster":["id"],"setDefaultUser":["uuid"],"launchCluster":["id","uuid"],"getClusters":[],"getDefaultUser":["fallback"],"getRunningProcessesByClusterId":["cluster_id"],"getLogs":["id"],"getGameVersions":[],"openMsaLogin":[],"getPackage":["provider","slug"],"createCluster":["options"],"removeUser":["uuid"],"readSettings":[],"getUsersFromAuthor":["provider","author"],"writeSettings":["setting"],"getMultiplePackages":["provider","slugs"],"getClusterById":["id"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' }
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
removeCluster: (id: number) => Promise<null>,
@ -262,6 +262,7 @@ killProcess: (pid: number) => Promise<null>,
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
getGlobalProfile: () => Promise<SettingProfileModel>,
updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise<SettingProfileModel>,
createSettingsProfile: (name: string) => Promise<SettingProfileModel>,
getGameVersions: () => Promise<Version[]>,
getLoadersForVersion: (mcVersion: string) => Promise<GameLoader[]>,
getUsers: () => Promise<MinecraftCredentials[]>,
@ -282,11 +283,11 @@ getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<Manag
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
open: (input: string) => Promise<null> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'oneclient': { openDevTools: () => Promise<void>,
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> } };

View file

@ -1,7 +1,7 @@
import type { Settings } from '@/bindings.gen';
import type { ClusterModel, ProfileUpdate, SettingProfileModel, Settings } from '@/bindings.gen';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
export function useSettings() {
const settings = useCommandSuspense(['readSettings'], bindings.core.readSettings);
@ -23,3 +23,46 @@ export function useSettings() {
return { setting, setSetting, createSetting };
}
const emptyUpdate: ProfileUpdate = {
res: null,
force_fullscreen: null,
mem_max: null,
launch_args: null,
launch_env: null,
hook_pre: null,
hook_wrapper: null,
hook_post: null,
};
export function useClusterProfile(cluster: ClusterModel) {
const [profileName, setProfileName] = useState(cluster.setting_profile_name);
const profileSrc = useCommandSuspense(['getProfileOrDefault', profileName], () => bindings.core.getProfileOrDefault(profileName));
const [profile, setProfile] = useState(profileSrc.data);
useEffect(() => {
setProfile(profileSrc.data);
}, [profileSrc.data]);
const updateProfile = useMemo(() =>
async (update: Partial<ProfileUpdate>) => {
setProfile(profile => ({ ...profile, ...update }));
let name = profileName;
if (!profileName) {
name = globalThis.crypto.randomUUID();
await bindings.core.createSettingsProfile(name);
await bindings.core.updateClusterById(cluster.id, {
setting_profile_name: cluster.name,
name: null,
icon_url: null,
});
setProfileName(name);
}
if (!name)
throw new Error('No settings profile name');
await bindings.core.updateClusterProfile(name, { ...emptyUpdate, ...update });
profileSrc.refetch();
}, [profileName, cluster.name, cluster.id]);
// const updateProfile = useMemo(() => (update: Partial<ProfileUpdate>) => setProfile({ ...profile.data, ...update }), [setProfile, profile.data]);
return { profile, setProfile, updateProfile };
}

View file

@ -73,6 +73,7 @@ function RouteComponent() {
<Tab from={Route.id} search={search} to="/app/cluster/overview">Overview</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>
{isRunning && <span className="flex-1"></span>}
{isRunning && <Tab from={Route.id} search={search} to="/app/cluster/process">Process</Tab>}
</TabList>

View file

@ -0,0 +1,116 @@
import { SheetPage } from '@/components';
import SettingsRow from '@/components/SettingsRow';
import { useClusterProfile } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { Switch, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react';
export const Route = createFileRoute('/app/cluster/settings')({
component: RouteComponent,
});
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { profile, updateProfile } = useClusterProfile(cluster);
return (
<SheetPage.Content>
<h1>Settings</h1>
<SettingsRow.Header>Process</SettingsRow.Header>
<SettingsRow
description="Command to run before launching the game."
icon={<FilePlus02Icon />}
title="Pre-Launch Command"
>
<TextField
onChange={e => updateProfile({ hook_pre: e.currentTarget.value })}
placeholder="echo 'Game started'"
value={profile.hook_pre ?? undefined}
/>
</SettingsRow>
<SettingsRow
description="Command to run when launching the game."
icon={<ParagraphWrapIcon />}
title="Wrapper Command"
>
<TextField
onChange={e => updateProfile({ hook_wrapper: e.currentTarget.value })}
placeholder="gamescope"
value={profile.hook_wrapper ?? undefined}
/>
</SettingsRow>
<SettingsRow
description="Command to run after exiting the game."
icon={<FileX02Icon />}
title="Post-Exit Command"
>
<TextField
onChange={e => updateProfile({ hook_post: e.currentTarget.value })}
placeholder="echo 'Game exited'"
value={profile.hook_post ?? undefined}
/>
</SettingsRow>
<SettingsRow.Header>Game</SettingsRow.Header>
<SettingsRow
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
title="Force Fullscreen"
>
<Switch
isSelected={profile.force_fullscreen ?? false}
onChange={val => updateProfile({ force_fullscreen: val })}
/>
</SettingsRow>
<SettingsRow
description="The game window resolution in pixels."
icon={<LayoutTopIcon />}
title="Resolution"
>
<div className="grid grid-cols-[70px_16px_70px] gap-2 grid-justify-center grid-items-center">
<TextField
className="text-center"
onChange={(e) => {
updateProfile({ res: { height: profile.res?.height ?? 720, width: Number(e.currentTarget.value) } });
}}
type="number"
value={profile.res?.width}
/>
<XIcon className="size-4 self-center" />
<TextField
className="text-center"
onChange={(e) => {
updateProfile({ res: { width: profile.res?.height ?? 1280, height: Number(e.currentTarget.value) } });
}}
type="number"
value={profile.res?.height}
/>
</div>
</SettingsRow>
<SettingsRow
description="The amount of memory in megabytes allocated for the game."
icon={<Database01Icon />}
title="Memory"
>
<div className="flex items-center gap-x-4 flex-justify-center">
<div className="flex flex-row items-center gap-x-2">
<TextField
className="text-center"
onChange={(e) => {
updateProfile({ mem_max: Number(e.currentTarget.value) });
}}
type="number"
value={profile.mem_max ?? undefined}
/>
</div>
</div>
</SettingsRow>
</SheetPage.Content>
);
}

View file

@ -4,6 +4,7 @@ use crate::api::packages::data::{
};
use crate::api::packages::modpack::data::ModpackArchive;
use crate::api::packages::provider::ProviderExt;
use crate::api::setting_profiles::get_global_profile;
use crate::store::processes::Process;
use crate::store::{Settings, State};
use crate::utils::pagination::Paginated;
@ -89,6 +90,9 @@ pub trait TauriLauncherApi {
name: String,
profile: ProfileUpdate,
) -> LauncherResult<SettingsProfile>;
#[taurpc(alias = "createSettingsProfile")]
async fn create_settings_profile(name: String) -> LauncherResult<SettingsProfile>;
// endregion: profiles
// MARK: API: metadata
@ -215,6 +219,7 @@ pub struct CreateCluster {
pub struct ClusterUpdate {
name: Option<String>,
icon_url: Option<Icon>,
setting_profile_name: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type, Clone)]
@ -323,6 +328,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::cluster::dao::set_icon_by_id(id, &icon_url).await?;
}
if let Some(setting_profile_name) = request.setting_profile_name {
active_model.setting_profile_name = Set(Some(setting_profile_name))
}
Ok(active_model)
},
)
@ -396,6 +405,13 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::setting_profiles::dao::get_profile_or_default(name.as_ref()).await
}
async fn create_settings_profile(self, name: String) -> LauncherResult<SettingsProfile> {
let mut profile = get_global_profile().await;
profile.name = name;
api::setting_profiles::dao::insert_profile(profile.into()).await
}
// please kill this with fire
async fn update_cluster_profile(
self,