feat: Cluster + Global settings (with overrides!)
This commit is contained in:
parent
574d783821
commit
1b46c5da2e
14 changed files with 662 additions and 132 deletions
|
|
@ -68,9 +68,31 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Updates the cluster with the given UUID. The cluster only updates game setting fields
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn edit_cluster(
|
||||
pub async fn edit_game_settings(uuid: Uuid, new_cluster: Cluster) -> Result<(), String> {
|
||||
let cluster_path = ClusterPath::find_by_uuid(uuid).await?;
|
||||
|
||||
cluster::edit(&cluster_path, |old| {
|
||||
// Game
|
||||
old.force_fullscreen = new_cluster.force_fullscreen;
|
||||
old.resolution = new_cluster.resolution;
|
||||
old.memory = new_cluster.memory;
|
||||
|
||||
// Process
|
||||
|
||||
async move { Ok(()) }
|
||||
}).await?;
|
||||
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn edit_cluster_meta(
|
||||
uuid: Uuid,
|
||||
name: Option<String>,
|
||||
icon_path: Option<String>,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ macro_rules! collect_commands {
|
|||
remove_user,
|
||||
// Cluster
|
||||
create_cluster,
|
||||
edit_cluster,
|
||||
edit_game_settings,
|
||||
edit_cluster_meta,
|
||||
remove_cluster,
|
||||
get_cluster,
|
||||
get_clusters,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Transition, type TransitionProps } from 'solid-transition-group';
|
|||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
|
||||
function AnimatedRoutes(props: TransitionProps & ParentProps) {
|
||||
const settings = useSettingsContext();
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
const defaultProps: TransitionProps = {
|
||||
mode: 'outin',
|
||||
|
|
@ -20,7 +20,7 @@ function AnimatedRoutes(props: TransitionProps & ParentProps) {
|
|||
|
||||
return (
|
||||
<Show
|
||||
when={settings.disable_animations !== true}
|
||||
when={settings().disable_animations !== true}
|
||||
fallback={(
|
||||
<>{split.children}</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { JSX, ParentProps } from 'solid-js';
|
||||
|
||||
type SettingsRowProps = ParentProps & {
|
||||
export type SettingsRowProps = ParentProps & {
|
||||
title: JSX.Element;
|
||||
description: JSX.Element;
|
||||
icon: JSX.Element;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function TitlebarButton(props: TitlebarButtonProps) {
|
|||
}
|
||||
|
||||
function WindowFrame() {
|
||||
const settings = useSettingsContext();
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
const maximize = () => Window.getCurrent().toggleMaximize();
|
||||
const minimize = () => Window.getCurrent().minimize();
|
||||
|
|
@ -43,7 +43,7 @@ function WindowFrame() {
|
|||
|
||||
onMount(() => {
|
||||
Window.getCurrent().onCloseRequested((event) => {
|
||||
if (settings.hide_close_prompt !== true) {
|
||||
if (settings().hide_close_prompt !== true) {
|
||||
event.preventDefault();
|
||||
modal.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@
|
|||
@apply bg-brand/30;
|
||||
}
|
||||
|
||||
@apply bg-transparent outline-none appearance-none p-0 m-0 box-border w-full;
|
||||
@apply bg-transparent outline-none appearance-none p-0 m-0 box-border w-full font-medium;
|
||||
|
||||
&::placeholder {
|
||||
@apply text-fg-secondary/40 font-normal;
|
||||
}
|
||||
}
|
||||
|
||||
& span {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
import { type JSX, createSignal, splitProps } from 'solid-js';
|
||||
import { type Accessor, type JSX, createEffect, createSignal, splitProps } from 'solid-js';
|
||||
|
||||
type ToggleProps = JSX.HTMLAttributes<HTMLDivElement> & {
|
||||
defaultChecked?: boolean;
|
||||
checked?: Accessor<boolean>;
|
||||
onChecked?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
function Toggle(props: ToggleProps) {
|
||||
const [checked, setChecked] = createSignal(props.defaultChecked || false);
|
||||
const [split, rest] = splitProps(props, ['class', 'defaultChecked', 'onChecked', 'text']);
|
||||
// eslint-disable-next-line solid/reactivity -- ok
|
||||
const [checked, setChecked] = createSignal(props.checked?.());
|
||||
const [split, rest] = splitProps(props, ['class', 'checked', 'onChecked', 'text']);
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- i hate mylife
|
||||
if (props.checked)
|
||||
createEffect(() => {
|
||||
setChecked(props.checked!());
|
||||
});
|
||||
|
||||
function toggle() {
|
||||
const newValue = !checked();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { type Context, type ParentProps, type Resource, Show, createContext, createEffect, useContext } from 'solid-js';
|
||||
import { type Context, type ParentProps, Show, createContext, createEffect, useContext } from 'solid-js';
|
||||
import { useBeforeLeave } from '@solidjs/router';
|
||||
import useCommand from './useCommand';
|
||||
import type { Settings } from '~bindings';
|
||||
import { bridge } from '~imports';
|
||||
|
||||
const SettingsContext = createContext<Settings>() as Context<Settings>;
|
||||
|
||||
/**
|
||||
* Sync the launcher state (CSS, text etc) with the current settings
|
||||
*/
|
||||
|
|
@ -12,22 +11,43 @@ export function syncSettings(settings: Settings) {
|
|||
document.body.classList.toggle('reduce-motion', settings.disable_animations);
|
||||
}
|
||||
|
||||
export function getSettings(): Resource<Settings> | undefined {
|
||||
const [resource] = useCommand(bridge.commands.getSettings);
|
||||
return resource;
|
||||
interface SettingsControllerType {
|
||||
settings: () => Settings;
|
||||
saveOnLeave: (settings: () => Partial<Settings>) => void;
|
||||
save: (settings: Settings) => void;
|
||||
}
|
||||
|
||||
const SettingsContext = createContext() as Context<SettingsControllerType>;
|
||||
|
||||
export function SettingsProvider(props: ParentProps) {
|
||||
const settings = getSettings();
|
||||
const [settings, { refetch }] = useCommand(bridge.commands.getSettings);
|
||||
|
||||
createEffect(() => {
|
||||
if (settings !== undefined && settings() !== undefined)
|
||||
syncSettings(settings!()!);
|
||||
});
|
||||
|
||||
const controller: SettingsControllerType = {
|
||||
settings: () => settings!()!,
|
||||
saveOnLeave: (settings) => {
|
||||
useBeforeLeave(() => {
|
||||
controller.save({
|
||||
...controller.settings(),
|
||||
...settings(),
|
||||
});
|
||||
});
|
||||
},
|
||||
save: (settings) => {
|
||||
bridge.commands.setSettings(settings).then(() => {
|
||||
syncSettings(settings);
|
||||
refetch();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={settings !== undefined && settings() !== undefined}>
|
||||
<SettingsContext.Provider value={settings!()!}>
|
||||
<SettingsContext.Provider value={controller}>
|
||||
{props.children}
|
||||
</SettingsContext.Provider>
|
||||
</Show>
|
||||
|
|
@ -40,21 +60,7 @@ export function useSettingsContext() {
|
|||
if (!context)
|
||||
throw new Error('useSettingsContext should be called inside its SettingsProvider');
|
||||
|
||||
const contextProxy = new Proxy(context, {
|
||||
set(target, key, value) {
|
||||
// @ts-expect-error typescript
|
||||
target[key] = value;
|
||||
save(target); // TODO: Possibly make a "Save confirmation" instead of writing to the settings file every time?
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return contextProxy;
|
||||
}
|
||||
|
||||
function save(settings: Settings) {
|
||||
bridge.commands.setSettings(settings);
|
||||
syncSettings(settings);
|
||||
return context;
|
||||
}
|
||||
|
||||
export default useSettingsContext;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import FormattedLog from '~ui/components/content/FormattedLog';
|
|||
function ClusterLogs() {
|
||||
const [cluster] = useClusterContext();
|
||||
const [logs] = useCommand(bridge.commands.getClusterLogs, cluster()!.uuid);
|
||||
const settings = useSettingsContext();
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
const [activeLogFile, setActiveLogFile] = createSignal<string | null>(null);
|
||||
const [logContent, setLogContent] = createSignal<string | null>(null);
|
||||
|
|
@ -31,7 +31,7 @@ function ClusterLogs() {
|
|||
}
|
||||
|
||||
async function openFolder() {
|
||||
const root = settings.config_dir;
|
||||
const root = settings().config_dir;
|
||||
const path = cluster()!.path;
|
||||
if (root === null || root === undefined || path === null || path === undefined)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import type { ModalProps } from '~ui/components/overlay/Modal';
|
|||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
||||
|
||||
function ClusterOverview() {
|
||||
const settings = useSettingsContext();
|
||||
const { settings } = useSettingsContext();
|
||||
const [cluster, { refetch }] = useClusterContext();
|
||||
|
||||
const [editMode, setEditMode] = createSignal(false);
|
||||
|
|
@ -47,7 +47,7 @@ function ClusterOverview() {
|
|||
|
||||
function getPath() {
|
||||
const clusterPath = cluster()?.path;
|
||||
const configDir = settings.config_dir;
|
||||
const configDir = settings().config_dir;
|
||||
|
||||
if (typeof clusterPath !== 'string' || typeof configDir !== 'string')
|
||||
return '';
|
||||
|
|
@ -95,7 +95,7 @@ function ClusterOverview() {
|
|||
if (!c)
|
||||
return;
|
||||
|
||||
await bridge.commands.editCluster(
|
||||
await bridge.commands.editClusterMeta(
|
||||
c.uuid,
|
||||
newName().length > 0 ? newName() : null,
|
||||
newCover().length > 0 ? newCover() : c.meta.icon_url || c.meta.icon || null,
|
||||
|
|
|
|||
|
|
@ -1,34 +1,305 @@
|
|||
import { useBeforeLeave } from '@solidjs/router';
|
||||
import { type Accessor, type Setter, Show, createEffect, createSignal, splitProps, untrack } from 'solid-js';
|
||||
import { CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { createSetting } from '../settings/game/SettingsMinecraft';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import type { SettingsRowProps } from '~ui/components/SettingsRow';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
import BaseSettingsRow from '~ui/components/SettingsRow';
|
||||
import type { Cluster } from '~bindings';
|
||||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
import SettingsMinecraft from '~ui/pages/settings/game/SettingsMinecraft';
|
||||
import Toggle from '~ui/components/base/Toggle';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
import { asEnvVariables } from '~utils';
|
||||
import { tryResult } from '~ui/hooks/useCommand';
|
||||
import { bridge } from '~imports';
|
||||
|
||||
function ClusterSettings() {
|
||||
const [cluster] = useClusterContext();
|
||||
const settings = useSettingsContext();
|
||||
|
||||
// TODO: Save cluster settings
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Overview</h1>
|
||||
<ScrollableContainer>
|
||||
<SettingsMinecraft.Settings
|
||||
fullscreen={{
|
||||
get: cluster()?.force_fullscreen ?? settings.force_fullscreen ?? false,
|
||||
set: value => cluster() && (cluster()!.force_fullscreen = value),
|
||||
isGlobal: cluster()?.force_fullscreen === null,
|
||||
}}
|
||||
resolution={{
|
||||
get: cluster()?.resolution ?? settings.resolution,
|
||||
set: value => cluster() && (cluster()!.resolution = value),
|
||||
isGlobal: cluster()?.resolution === undefined || cluster()?.resolution === null,
|
||||
}}
|
||||
/>
|
||||
<Show when={cluster() !== undefined}>
|
||||
{PageSettings(() => cluster()!)}
|
||||
</Show>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsRow(props: SettingsRowProps & {
|
||||
reset: () => any;
|
||||
isGlobal: Accessor<boolean>;
|
||||
}) {
|
||||
const [split, rest] = splitProps(props, ['children', 'reset', 'isGlobal']);
|
||||
|
||||
// TODO: Reset button doesn't show up immediately after changing a setting
|
||||
|
||||
return (
|
||||
<BaseSettingsRow {...rest}>
|
||||
<Show when={split.isGlobal() === false}>
|
||||
<div
|
||||
class="px-2 py-1 text-xs text-white bg-brand hover:bg-brand-hover active:bg-brand-pressed rounded-md font-italic font-medium"
|
||||
onClick={() => split.reset()}
|
||||
>
|
||||
Reset
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{split.children}
|
||||
</BaseSettingsRow>
|
||||
);
|
||||
}
|
||||
|
||||
function PageSettings(cluster: Accessor<Cluster>) {
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
// Game
|
||||
const fullscreen = createSetting(cluster().force_fullscreen, settings().force_fullscreen ?? false);
|
||||
const resolution = createSetting(cluster().resolution, settings().resolution);
|
||||
const memory = createSetting(cluster().memory, settings().memory);
|
||||
|
||||
// Launcher
|
||||
|
||||
// Process
|
||||
const preCommand = createSetting(cluster().init_hooks?.pre, settings().init_hooks.pre ?? '');
|
||||
const wrapperCommand = createSetting(cluster().init_hooks?.wrapper, settings().init_hooks.wrapper ?? '');
|
||||
const postCommand = createSetting(cluster().init_hooks?.post, settings().init_hooks.post ?? '');
|
||||
|
||||
// JVM
|
||||
const javaArgs = createSetting(cluster().java?.custom_arguments, settings().custom_java_args);
|
||||
const envVars = createSetting(cluster().java?.custom_env_arguments, settings().custom_env_args);
|
||||
|
||||
useBeforeLeave(() => {
|
||||
tryResult(bridge.commands.editGameSettings, cluster().uuid, {
|
||||
...cluster(),
|
||||
|
||||
// Game
|
||||
force_fullscreen: fullscreen.getRaw(),
|
||||
resolution: resolution.getRaw(),
|
||||
memory: memory.getRaw(),
|
||||
|
||||
// Process
|
||||
init_hooks: {
|
||||
pre: preCommand.getRaw(),
|
||||
wrapper: wrapperCommand.getRaw(),
|
||||
post: postCommand.getRaw(),
|
||||
},
|
||||
|
||||
// JVM
|
||||
java: {
|
||||
custom_arguments: javaArgs.getRaw(),
|
||||
custom_env_arguments: envVars.getRaw(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const GameSettings = () => (
|
||||
<>
|
||||
<BaseSettingsRow.Header>Game</BaseSettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Force Fullscreen"
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
isGlobal={fullscreen.isGlobal}
|
||||
reset={fullscreen.resetToFallback}
|
||||
>
|
||||
<Toggle
|
||||
checked={fullscreen.get}
|
||||
onChecked={fullscreen.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Resolution"
|
||||
description="The game window resolution in pixels."
|
||||
icon={<LayoutTopIcon />}
|
||||
isGlobal={resolution.isGlobal}
|
||||
reset={resolution.resetToFallback}
|
||||
>
|
||||
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={resolution.get()[0]}
|
||||
onValidSubmit={(value) => {
|
||||
resolution.set([Number.parseInt(value), resolution.get()[1]]);
|
||||
}}
|
||||
/>
|
||||
<XIcon class="w-4 h-4" />
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={resolution.get()[1]}
|
||||
onValidSubmit={(value) => {
|
||||
resolution.set([resolution.get()[0], Number.parseInt(value)]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Memory"
|
||||
description="The amount of memory in megabytes allocated for the game."
|
||||
icon={<Database01Icon />}
|
||||
isGlobal={memory.isGlobal}
|
||||
reset={memory.resetToFallback}
|
||||
>
|
||||
<div class="flex flex-justify-center items-center gap-x-4">
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<span>Min:</span>
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
labelClass="w-[70px]!"
|
||||
value={memory.get().minimum}
|
||||
onValidSubmit={(value) => {
|
||||
memory.set({ minimum: Number.parseInt(value), maximum: memory.get().maximum });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<span>Max:</span>
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
labelClass="w-[70px]!"
|
||||
value={memory.get().maximum}
|
||||
onValidSubmit={(value) => {
|
||||
memory.set({ minimum: memory.get().minimum, maximum: Number.parseInt(value) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const LauncherSettings = () => (
|
||||
<>
|
||||
{/* <BaseSettingsRow.Header>Launcher</BaseSettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Hide On Launch"
|
||||
description="Hide the launcher whenever you start a game."
|
||||
icon={<EyeIcon />}
|
||||
>
|
||||
<Toggle
|
||||
checked={hideOnLaunch.get}
|
||||
onChecked={hideOnLaunch.set}
|
||||
/>
|
||||
</SettingsRow> */}
|
||||
</>
|
||||
);
|
||||
|
||||
const ProcessSettings = () => (
|
||||
<>
|
||||
<BaseSettingsRow.Header>Process</BaseSettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Pre-Launch Command"
|
||||
description="Command to run before launching the game."
|
||||
icon={<FilePlus02Icon />}
|
||||
isGlobal={preCommand.isGlobal}
|
||||
reset={preCommand.resetToFallback}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="echo 'Game started'"
|
||||
value={preCommand.get()}
|
||||
onValidSubmit={preCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Wrapper Command"
|
||||
description="Command to run when launching the game."
|
||||
icon={<ParagraphWrapIcon />}
|
||||
isGlobal={wrapperCommand.isGlobal}
|
||||
reset={wrapperCommand.resetToFallback}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="gamescope"
|
||||
value={wrapperCommand.get()}
|
||||
onValidSubmit={wrapperCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Post-Exit Command"
|
||||
description="Command to run after exiting the game."
|
||||
icon={<FileX02Icon />}
|
||||
isGlobal={postCommand.isGlobal}
|
||||
reset={postCommand.resetToFallback}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="echo 'Game exited'"
|
||||
value={postCommand.get()}
|
||||
onValidSubmit={postCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const JvmSettings = () => (
|
||||
<>
|
||||
<BaseSettingsRow.Header>Java</BaseSettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Version"
|
||||
description="Choose the JRE (Java Runtime Environment) used for the game."
|
||||
icon={<CpuChip01Icon />}
|
||||
isGlobal={() => true}
|
||||
reset={() => {}}
|
||||
/>
|
||||
|
||||
<SettingsRow
|
||||
title="Arguments"
|
||||
description="Additional arguments passed to the JVM (Java Virtual Machine)."
|
||||
icon={<FilePlus02Icon />}
|
||||
isGlobal={javaArgs.isGlobal}
|
||||
reset={javaArgs.resetToFallback}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder='"-Xmx3G"'
|
||||
value={javaArgs.get().join(' ')}
|
||||
onValidSubmit={(value) => {
|
||||
javaArgs.set(value.split(' '));
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Environment Variables"
|
||||
description="Additional environment variables passed to the JVM (Java Virtual Machine)."
|
||||
icon={<VariableIcon />}
|
||||
isGlobal={envVars.isGlobal}
|
||||
reset={envVars.resetToFallback}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder='"JAVA_HOME=/path/to/java"'
|
||||
value={envVars.get().join(' ')}
|
||||
onValidSubmit={(value) => {
|
||||
envVars.set(asEnvVariables(value));
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GameSettings />
|
||||
<LauncherSettings />
|
||||
<ProcessSettings />
|
||||
<JvmSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterSettings;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Sidebar from '~ui/components/Sidebar';
|
|||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
|
||||
function SettingsDeveloper() {
|
||||
const settings = useSettingsContext();
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
|
|
@ -31,8 +31,8 @@ function SettingsDeveloper() {
|
|||
icon={<GitMergeIcon />}
|
||||
>
|
||||
<Toggle
|
||||
defaultChecked={settings.debug_mode ?? false}
|
||||
onChecked={value => settings.debug_mode = value}
|
||||
checked={() => settings().debug_mode ?? false}
|
||||
onChecked={value => settings().debug_mode = value}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,106 +1,309 @@
|
|||
import { EyeIcon, LayoutTopIcon, Maximize01Icon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { Show } from 'solid-js';
|
||||
import { CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { type Accessor, type Setter, createSignal, onMount, untrack } from 'solid-js';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
import Toggle from '~ui/components/base/Toggle';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import SettingsRow from '~ui/components/SettingsRow';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
import { asEnvVariables } from '~utils';
|
||||
|
||||
// TODO: REFACTOR THIS ENTIRE FILE AND POSSIBLY `apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx` TOO
|
||||
|
||||
function SettingsMinecraft() {
|
||||
const settings = useSettingsContext();
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Global Minecraft Settings</h1>
|
||||
<ScrollableContainer>
|
||||
<SettingsMinecraft.Settings
|
||||
fullscreen={{
|
||||
get: settings.force_fullscreen ?? false,
|
||||
set: value => settings.force_fullscreen = value,
|
||||
}}
|
||||
resolution={{
|
||||
get: settings.resolution,
|
||||
set: value => settings.resolution = value,
|
||||
}}
|
||||
hide_on_launch={{
|
||||
get: settings.hide_on_launch ?? false,
|
||||
set: value => settings.hide_on_launch = value,
|
||||
}}
|
||||
/>
|
||||
<PageSettings />
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingsEntryType<T> {
|
||||
get: T;
|
||||
set: (value: T) => any;
|
||||
isGlobal?: boolean; // TODO: Ask how "global settings sync" should work on pages like ClusterSettings
|
||||
interface CreateSetting<T> {
|
||||
get: Accessor<T>;
|
||||
getRaw: () => T;
|
||||
set: Setter<T>;
|
||||
isGlobal: Accessor<boolean>;
|
||||
resetToFallback: (raw?: any) => void;
|
||||
};
|
||||
export function createSetting<T>(initial: T | undefined | null, fallback?: T): CreateSetting<T> {
|
||||
const [value, setValue] = createSignal<T>(initial || fallback as T);
|
||||
const [raw, setRaw] = createSignal<T>(initial as T);
|
||||
const [isGlobal, setIsGlobal] = createSignal(true);
|
||||
|
||||
interface SettingsProps {
|
||||
fullscreen: SettingsEntryType<boolean>;
|
||||
resolution: SettingsEntryType<[number, number]>;
|
||||
hide_on_launch: SettingsEntryType<boolean>;
|
||||
};
|
||||
const checkGlobal = () => {
|
||||
const check_one = untrack(() => value()) === undefined || untrack(() => value()) === null;
|
||||
const check_two = initial === undefined || initial === null;
|
||||
// const check_three = untrack(() => raw()) === undefined || untrack(() => raw()) === null;
|
||||
|
||||
SettingsMinecraft.Settings = function (props: Partial<SettingsProps>) {
|
||||
return (
|
||||
setIsGlobal(check_one || check_two);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
checkGlobal();
|
||||
});
|
||||
|
||||
// @ts-expect-error -- aaa
|
||||
const set: Setter<T> = (value) => {
|
||||
// @ts-expect-error -- aaa
|
||||
setRaw(value);
|
||||
// @ts-expect-error -- aaa
|
||||
setValue(value);
|
||||
|
||||
checkGlobal();
|
||||
};
|
||||
|
||||
const resetToFallback = (raw?: T) => {
|
||||
if (fallback === undefined || fallback === null)
|
||||
throw new Error('No fallback value provided');
|
||||
|
||||
setValue(() => fallback);
|
||||
|
||||
// @ts-expect-error -- aaa
|
||||
setRaw(() => (raw || null));
|
||||
|
||||
checkGlobal();
|
||||
};
|
||||
|
||||
const getRaw = () => raw();
|
||||
|
||||
return {
|
||||
get: value,
|
||||
getRaw,
|
||||
set,
|
||||
isGlobal,
|
||||
resetToFallback,
|
||||
};
|
||||
}
|
||||
|
||||
function PageSettings() {
|
||||
const { settings, saveOnLeave } = useSettingsContext();
|
||||
|
||||
// Game
|
||||
const fullscreen = createSetting(settings().force_fullscreen ?? false);
|
||||
const resolution = createSetting(settings().resolution);
|
||||
const memory = createSetting(settings().memory);
|
||||
|
||||
// Launcher
|
||||
const hideOnLaunch = createSetting(settings().hide_on_launch ?? false);
|
||||
|
||||
// Process
|
||||
const preCommand = createSetting(settings().init_hooks.pre ?? '');
|
||||
const wrapperCommand = createSetting(settings().init_hooks.wrapper ?? '');
|
||||
const postCommand = createSetting(settings().init_hooks.post ?? '');
|
||||
|
||||
// JVM
|
||||
const javaArgs = createSetting(settings().custom_java_args);
|
||||
const envVars = createSetting(settings().custom_env_args);
|
||||
|
||||
saveOnLeave(() => ({
|
||||
// Game
|
||||
force_fullscreen: fullscreen.get(),
|
||||
resolution: resolution.get(),
|
||||
memory: memory.get(),
|
||||
|
||||
// Launcher
|
||||
hide_on_launch: hideOnLaunch.get(),
|
||||
|
||||
// Process
|
||||
init_hooks: {
|
||||
pre: preCommand.get(),
|
||||
wrapper: wrapperCommand.get(),
|
||||
post: postCommand.get(),
|
||||
},
|
||||
|
||||
// JVM
|
||||
custom_java_args: javaArgs.get(),
|
||||
custom_env_args: envVars.get(),
|
||||
}));
|
||||
|
||||
const GameSettings = () => (
|
||||
<>
|
||||
<Show when={props.fullscreen !== undefined}>
|
||||
<SettingsRow
|
||||
title="Force Fullscreen"
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
>
|
||||
<Toggle
|
||||
defaultChecked={props.fullscreen!.get}
|
||||
onChecked={props.fullscreen!.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||
|
||||
<Show when={props.resolution !== undefined}>
|
||||
<SettingsRow
|
||||
title="Resolution"
|
||||
description="The game window resolution in pixels."
|
||||
icon={<LayoutTopIcon />}
|
||||
>
|
||||
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
|
||||
<SettingsRow
|
||||
title="Force Fullscreen"
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
>
|
||||
<Toggle
|
||||
checked={fullscreen.get}
|
||||
onChecked={fullscreen.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Resolution"
|
||||
description="The game window resolution in pixels."
|
||||
icon={<LayoutTopIcon />}
|
||||
>
|
||||
<div class="grid grid-justify-center grid-items-center gap-2 grid-cols-[70px_16px_70px]">
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={resolution.get()[0]}
|
||||
onValidSubmit={(value) => {
|
||||
resolution.set([Number.parseInt(value), resolution.get()[1]]);
|
||||
}}
|
||||
/>
|
||||
<XIcon class="w-4 h-4" />
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={resolution.get()[1]}
|
||||
onValidSubmit={(value) => {
|
||||
resolution.set([resolution.get()[0], Number.parseInt(value)]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Memory"
|
||||
description="The amount of memory in megabytes allocated for the game."
|
||||
icon={<Database01Icon />}
|
||||
>
|
||||
<div class="flex flex-justify-center items-center gap-x-4">
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<span>Min:</span>
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={props.resolution!.get[0]}
|
||||
labelClass="w-[70px]!"
|
||||
value={memory.get().minimum}
|
||||
onValidSubmit={(value) => {
|
||||
props.resolution!.set([Number.parseInt(value), props.resolution!.get[1]]);
|
||||
}}
|
||||
/>
|
||||
<XIcon class="w-4 h-4" />
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
value={props.resolution!.get[1]}
|
||||
onValidSubmit={(value) => {
|
||||
props.resolution!.set([props.resolution!.get[0], Number.parseInt(value)]);
|
||||
memory.set({ minimum: Number.parseInt(value), maximum: memory.get().maximum });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
|
||||
<Show when={props.hide_on_launch !== undefined}>
|
||||
<SettingsRow
|
||||
title="Hide On Launch"
|
||||
description="Hide the launcher whenever you start a game."
|
||||
icon={<EyeIcon />}
|
||||
>
|
||||
<Toggle
|
||||
defaultChecked={props.hide_on_launch!.get}
|
||||
onChecked={props.hide_on_launch!.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<span>Max:</span>
|
||||
<TextField.Number
|
||||
class="text-center"
|
||||
labelClass="w-[70px]!"
|
||||
value={memory.get().maximum}
|
||||
onValidSubmit={(value) => {
|
||||
memory.set({ minimum: memory.get().minimum, maximum: Number.parseInt(value) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LauncherSettings = () => (
|
||||
<>
|
||||
<SettingsRow.Header>Launcher</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Hide On Launch"
|
||||
description="Hide the launcher whenever you start a game."
|
||||
icon={<EyeIcon />}
|
||||
>
|
||||
<Toggle
|
||||
checked={hideOnLaunch.get}
|
||||
onChecked={hideOnLaunch.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const ProcessSettings = () => (
|
||||
<>
|
||||
<SettingsRow.Header>Process</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Pre-Launch Command"
|
||||
description="Command to run before launching the game."
|
||||
icon={<FilePlus02Icon />}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="echo 'Game started'"
|
||||
value={preCommand.get()}
|
||||
onValidSubmit={preCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Wrapper Command"
|
||||
description="Command to run when launching the game."
|
||||
icon={<ParagraphWrapIcon />}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="gamescope"
|
||||
value={wrapperCommand.get()}
|
||||
onValidSubmit={wrapperCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Post-Exit Command"
|
||||
description="Command to run after exiting the game."
|
||||
icon={<FileX02Icon />}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder="echo 'Game exited'"
|
||||
value={postCommand.get()}
|
||||
onValidSubmit={postCommand.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const JvmSettings = () => (
|
||||
<>
|
||||
<SettingsRow.Header>Java</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
title="Version"
|
||||
description="Choose the JRE (Java Runtime Environment) used for the game."
|
||||
icon={<CpuChip01Icon />}
|
||||
/>
|
||||
|
||||
<SettingsRow
|
||||
title="Arguments"
|
||||
description="Additional arguments passed to the JVM (Java Virtual Machine)."
|
||||
icon={<FilePlus02Icon />}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder='"-Xmx3G"'
|
||||
value={javaArgs.get().join(' ')}
|
||||
onValidSubmit={(value) => {
|
||||
javaArgs.set(value.split(' '));
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title="Environment Variables"
|
||||
description="Additional environment variables passed to the JVM (Java Virtual Machine)."
|
||||
icon={<VariableIcon />}
|
||||
>
|
||||
<TextField
|
||||
labelClass="w-full! min-w-[260px]!"
|
||||
placeholder='"JAVA_HOME=/path/to/java"'
|
||||
value={envVars.get().join(' ')}
|
||||
onValidSubmit={(value) => {
|
||||
envVars.set(asEnvVariables(value));
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GameSettings />
|
||||
<LauncherSettings />
|
||||
<ProcessSettings />
|
||||
<JvmSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsMinecraft;
|
||||
|
|
|
|||
|
|
@ -80,3 +80,19 @@ export function secondsToWords(seconds: number | bigint, plural: boolean = true)
|
|||
|
||||
return str;
|
||||
}
|
||||
|
||||
export function asEnvVariables(str: string): [string, string][] {
|
||||
return str
|
||||
.split(' ')
|
||||
.map((pair) => {
|
||||
const split = pair.split('=', 1);
|
||||
|
||||
if (split.length === 0)
|
||||
return null;
|
||||
else if (split.length === 1)
|
||||
return [split[0]!, ''] as [string, string];
|
||||
else
|
||||
return [split[0]!, split[1]!] as [string, string];
|
||||
})
|
||||
.filter(pair => pair !== null);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue