feat: implement new settings UI and components
This commit is contained in:
parent
46cca44837
commit
12eea80950
13 changed files with 561 additions and 48 deletions
|
|
@ -218,7 +218,7 @@
|
|||
|
||||
use std::ops::Deref;
|
||||
|
||||
use onelauncher_core::{api::cluster, entity::{clusters::Model, icon::Icon, loader::GameLoader}, error::LauncherResult};
|
||||
use onelauncher_core::{api::{cluster, setting_profiles}, entity::{clusters::Model, icon::Icon, loader::GameLoader}, error::LauncherResult, migration::sea_orm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use crate::api::error::SerializableResult;
|
||||
|
|
@ -237,7 +237,14 @@ pub struct CreateCluster {
|
|||
pub async fn create_cluster(options: CreateCluster) -> SerializableResult<Model> {
|
||||
let thing = cluster::create_cluster(&options.name, &options.mc_version, options.mc_loader, options.mc_loader_version.as_deref(), options.icon_url).await?;
|
||||
|
||||
cluster::prepare_cluster(&mut thing.clone(), Some(false));
|
||||
// cluster::prepare_cluster(&mut thing.clone(), Some(false));
|
||||
|
||||
if setting_profiles::dao::get_profile_by_name(&options.name).await?.is_none() {
|
||||
setting_profiles::create_profile(&options.name, async |mut profile| {
|
||||
profile.mem_max = sea_orm::ActiveValue::Set(Some(2048));
|
||||
Ok(profile)
|
||||
}).await?;
|
||||
}
|
||||
|
||||
Ok(thing)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use onelauncher_core::entity::setting_profiles::Model;
|
||||
use onelauncher_core::entity::setting_profiles::Model as SettingsModel;
|
||||
use onelauncher_core::api::setting_profiles;
|
||||
|
||||
use crate::api::error::SerializableResult;
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_global_profile() -> SerializableResult<Model> {
|
||||
pub async fn get_global_profile() -> SerializableResult<SettingsModel> {
|
||||
Ok(setting_profiles::get_global_profile().await)
|
||||
}
|
||||
|
||||
|
|
@ -13,6 +13,6 @@ pub async fn get_global_profile() -> SerializableResult<Model> {
|
|||
#[tauri::command]
|
||||
pub async fn get_profile_or_default(
|
||||
name: Option<String>,
|
||||
) -> SerializableResult<Model> {
|
||||
) -> SerializableResult<SettingsModel> {
|
||||
Ok(setting_profiles::dao::get_profile_or_default(name.as_ref()).await?)
|
||||
}
|
||||
|
|
@ -5,26 +5,37 @@ import { Switch as AriaSwitch } from 'react-aria-components';
|
|||
import { tv } from 'tailwind-variants';
|
||||
|
||||
export const switchVariants = tv({
|
||||
base: [
|
||||
'w-[40px] h-[22px] p-3 flex flex-row relative rounded-full transition-colors overflow-hidden bg-brand',
|
||||
],
|
||||
})
|
||||
base: [
|
||||
'group inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
'bg-component-bg-pressed data-[selected]:bg-blue-600',
|
||||
'focus:outline-none',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
],
|
||||
});
|
||||
|
||||
export const switchThumbVariants = tv({
|
||||
base: [
|
||||
'pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform',
|
||||
'translate-x-1 group-data-[selected]:translate-x-6',
|
||||
],
|
||||
});
|
||||
|
||||
export type SwitchProps = AriaSwitchProps & RefAttributes<HTMLLabelElement> & {
|
||||
className?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SwitchProps) {
|
||||
return (
|
||||
<AriaSwitch className={switchVariants({ className })} {...props}>
|
||||
<div className="bg-orange-400" />
|
||||
{props.children}
|
||||
return (
|
||||
<AriaSwitch className={switchVariants({ className })} {...props}>
|
||||
<span className={switchThumbVariants()} />
|
||||
{children && <span className="ml-3 text-sm font-medium text-gray-900">{children}</span>}
|
||||
</AriaSwitch>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default Switch;
|
||||
export default Switch;
|
||||
59
apps/onelauncher/frontend/src/components/base/Tooltip.tsx
Normal file
59
apps/onelauncher/frontend/src/components/base/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type React from 'react';
|
||||
import type { RefAttributes } from 'react';
|
||||
import type { TooltipTriggerComponentProps as AriaTooltipTriggerProps } from 'react-aria-components';
|
||||
import {
|
||||
TooltipTrigger as AriaTooltipTrigger,
|
||||
Tooltip as AriaTooltip,
|
||||
OverlayArrow
|
||||
} from 'react-aria-components';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
export const tooltipVariants = tv({
|
||||
base: [
|
||||
'px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-lg',
|
||||
'border border-gray-700 max-w-xs z-50',
|
||||
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:zoom-in-95',
|
||||
'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:zoom-out-95',
|
||||
],
|
||||
});
|
||||
|
||||
export const tooltipArrowVariants = tv({
|
||||
base: [
|
||||
'fill-gray-900 stroke-gray-700 stroke-1',
|
||||
],
|
||||
});
|
||||
|
||||
export type TooltipProps = AriaTooltipTriggerProps & RefAttributes<HTMLElement> & {
|
||||
text: string;
|
||||
children: React.ReactElement;
|
||||
placement?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
delay?: number;
|
||||
closeDelay?: number;
|
||||
};
|
||||
|
||||
function Tooltip({
|
||||
text,
|
||||
children,
|
||||
placement = 'top',
|
||||
className,
|
||||
delay = 700,
|
||||
closeDelay = 0,
|
||||
...props
|
||||
}: TooltipProps) {
|
||||
return (
|
||||
<AriaTooltipTrigger delay={delay} closeDelay={closeDelay} {...props}>
|
||||
{children}
|
||||
<AriaTooltip placement={placement} className={tooltipVariants({ className })}>
|
||||
<OverlayArrow>
|
||||
<svg width={8} height={8} viewBox="0 0 8 8" className={tooltipArrowVariants()}>
|
||||
<path d="M0 0 L4 4 L8 0" />
|
||||
</svg>
|
||||
</OverlayArrow>
|
||||
{text}
|
||||
</AriaTooltip>
|
||||
</AriaTooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tooltip;
|
||||
|
|
@ -58,7 +58,7 @@ function RouteComponent() {
|
|||
<SettingsRow.Header>Cluster Actions</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
children={(
|
||||
<ToggleButton color='secondary'>Edit</ToggleButton>
|
||||
<Button color='secondary'>Edit</Button>
|
||||
)}
|
||||
description="Edit the cluster name and cover image."
|
||||
icon={<Edit02Icon />}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Sidebar from '../settings/route'
|
|||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import useCommand from '@/hooks/useCommand'
|
||||
import { bindings } from '@/main'
|
||||
import { GameSettings, ProcessSettings } from '../settings/minecraft'
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/settings')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -11,14 +12,22 @@ export const Route = createFileRoute('/app/cluster/settings')({
|
|||
function RouteComponent() {
|
||||
const { id } = Route.useSearch()
|
||||
|
||||
const result = useCommand("getProfileOrDefault", () => bindings.commands.getProfileOrDefault(id.toString()))
|
||||
const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint))
|
||||
const result = useCommand("getProfileOrDefault", () => bindings.commands.getProfileOrDefault(cluster.data?.name as string), {
|
||||
enabled: !!cluster.data?.name
|
||||
})
|
||||
|
||||
console.log(result.data)
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Settings</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<pre>{JSON.stringify(result.data, null, 2)}</pre>
|
||||
<h1>Minecraft Settings</h1>
|
||||
|
||||
<GameSettings />
|
||||
|
||||
<ProcessSettings />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
|||
import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg"
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { Server01Icon } from '@untitled-theme/icons-react';
|
||||
import HeaderImage from "@/assets/images/default_instance_cover.jpg";
|
||||
import HeaderImage from "@/assets/images/default_banner.png";
|
||||
import { TextField } from '@/components/base/TextField';
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -150,13 +151,13 @@ function ClusterCreate() {
|
|||
<Server01Icon className="h-8 w-8 text-fg-primary" />
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<h1 className="h-10 text-fg-primary -mt-2">New Cluster</h1>
|
||||
<span className="text-fg-primary">asd</span>
|
||||
{/* <span className="text-fg-primary">asd</span> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border border-white/5 rounded-b-lg">
|
||||
<div className="p-3">
|
||||
<pre>asdasdasdasds</pre>
|
||||
<TextField placeholder='Epik cluster name' className={"px-2"} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-x-2 p-3 pt-0">
|
||||
|
|
|
|||
133
apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx
Normal file
133
apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from './route'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import Button from '@/components/base/Button'
|
||||
import { InfoCircleIcon, Trash01Icon, UserPlus02Icon } from '@untitled-theme/icons-react'
|
||||
import PlayerHead from '@/components/content/PlayerHead'
|
||||
import { useState } from 'react'
|
||||
import { Show } from '@/components/base/Show'
|
||||
import Tooltip from '@/components/base/Tooltip'
|
||||
import useCommand from '@/hooks/useCommand'
|
||||
import { bindings } from '@/main'
|
||||
|
||||
export const Route = createFileRoute('/app/settings/accounts')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
interface Account {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: usersData, isLoading: usersLoading } = useCommand("getUsers", bindings.commands.getUsers)
|
||||
const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand("getDefaultUser", () => bindings.commands.getDefaultUser(false))
|
||||
|
||||
if (usersLoading || defaultUserLoading) {
|
||||
return <Sidebar.Page><h1>Accounts</h1><p>Loading accounts...</p></Sidebar.Page>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Accounts</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full space-y-2">
|
||||
<Show when={usersData && usersData.length > 0} fallback={<p>No accounts found.</p>}>
|
||||
{usersData?.map((account) => (
|
||||
<AccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
isCurrent={account.id === defaultUserData?.id}
|
||||
/>
|
||||
))}
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="mt-auto pt-2 flex flex-row items-end justify-end">
|
||||
<Button color="primary">
|
||||
<UserPlus02Icon /> Add Account
|
||||
</Button>
|
||||
</div>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
||||
interface AccountRowProps {
|
||||
account: Account;
|
||||
isCurrent?: boolean;
|
||||
};
|
||||
|
||||
function AccountRow({ account, isCurrent }: AccountRowProps) {
|
||||
const [errored, setErrored] = useState(false);
|
||||
|
||||
const setDefaultUserCommand = useCommand(
|
||||
"setDefaultUser",
|
||||
() => bindings.commands.setDefaultUser(account.id),
|
||||
{
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
}
|
||||
);
|
||||
|
||||
const removeUserCommand = useCommand(
|
||||
"removeUser",
|
||||
() => bindings.commands.removeUser(account.id),
|
||||
{
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
}
|
||||
);
|
||||
|
||||
const handleSetDefault = () => {
|
||||
setDefaultUserCommand.refetch();
|
||||
};
|
||||
|
||||
const handleDelete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
removeUserCommand.refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-row bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-xl gap-3.5 p-4 items-center box-border border ${isCurrent ? 'border-brand' : 'border-transparent'} cursor-pointer`} // Added cursor-pointer
|
||||
onClick={handleSetDefault}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleSetDefault(); }}
|
||||
>
|
||||
<div className="h-12 w-12 flex items-center justify-center flex-shrink-0">
|
||||
<PlayerHead
|
||||
className="h-12 w-12 rounded-md"
|
||||
onError={() => setErrored(true)}
|
||||
uuid={account.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col gap-1 flex-1 min-w-0 ${errored ? 'text-danger' : ''}`}>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<h3 className="text-lg font-medium truncate">{account.username}</h3>
|
||||
<Show when={errored}>
|
||||
<Tooltip
|
||||
text="Could not fetch this account's game profile"
|
||||
>
|
||||
<InfoCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<p className="text-xs text-fg-secondary truncate">{account.id}</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex-shrink-0">
|
||||
<Button
|
||||
color="danger"
|
||||
size='icon'
|
||||
onClick={handleDelete}
|
||||
aria-label={`Remove account ${account.username}`}
|
||||
>
|
||||
<Trash01Icon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx
Normal file
115
apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from './route'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import { THEMES } from '@/utils/theming'
|
||||
import SettingsRow from '@/components/SettingsRow'
|
||||
import { Monitor01Icon, PackageIcon, Speedometer04Icon } from '@untitled-theme/icons-react'
|
||||
import ToggleButton from '@/components/base/ToggleButton'
|
||||
import Switch from '@/components/base/Switch'
|
||||
|
||||
export const Route = createFileRoute('/app/settings/appearance')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<h1>Appearance</h1>
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<PrimaryThemeCard />
|
||||
<div className="grid grid-cols-3 h-full gap-4">
|
||||
{/* <For each={Object.keys(THEMES) as (keyof typeof THEMES)[]}>
|
||||
{theme => (
|
||||
<For each={THEMES[theme ?? 'OneLauncher'].variants}>
|
||||
{variant => (
|
||||
<ThemeCard
|
||||
setTheme={setTheme}
|
||||
themeSelector={`${theme}-${variant.name}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</For> */}
|
||||
|
||||
{Object.keys(THEMES).map((theme) => {
|
||||
return THEMES[theme as keyof typeof THEMES].variants.map((variant, i: number) => {
|
||||
return (
|
||||
<ThemeCard themeSelector={`${theme}-${variant.name}`} setTheme={() => { }} key={i} />
|
||||
)
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsRow
|
||||
description="Change the look of the package list."
|
||||
icon={<PackageIcon />}
|
||||
title="Package List Style"
|
||||
>
|
||||
<p>i hope i'll fix this</p>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Uses custom window frame for the launcher."
|
||||
icon={<Monitor01Icon />}
|
||||
title="Custom Window Frame"
|
||||
>
|
||||
<Switch />
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Disables all animations in the launcher."
|
||||
icon={<Speedometer04Icon />}
|
||||
title="Disable Animations"
|
||||
>
|
||||
<Switch />
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
||||
interface ThemeCardProps {
|
||||
themeSelector: string;
|
||||
setTheme: (merged: string) => void;
|
||||
};
|
||||
|
||||
function ThemeCard(props: ThemeCardProps) {
|
||||
return (
|
||||
<div className={`h-min theme-${props.themeSelector}`} onClick={() => props.setTheme(props.themeSelector)}>
|
||||
<svg fill="none" height="78" viewBox="0 0 126 78" width="126" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect className='fill-page-elevated' height="78" rx="8" width="126" />
|
||||
<rect height="77" rx="7.5" className='stroke-border' stroke-opacity="0.1" width="125" x="0.5" y="0.5" />
|
||||
<path d="M8 16H116" className='stroke-fg-primary' stroke-linecap="round" stroke-width="5" />
|
||||
<path d="M8 26H56" className="stroke-fg-secondary" stroke-linecap="round" stroke-width="3" />
|
||||
<rect className="fill-brand" fill-opacity="0.5" height="16" rx="4" width="64" x="54" y="54" />
|
||||
<path d="M54 58C54 55.7909 55.7909 54 58 54H86V70H58C55.7909 70 54 68.2091 54 66V58Z" fill="rgb(var(--clr-brand))" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PrimaryThemeCard() {
|
||||
return (
|
||||
<div className='w-[296] h-[183]'>
|
||||
<svg fill="none" height="183" viewBox="0 0 296 183" width="296" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect className="fill-page" height="183" rx="16" width="296" />
|
||||
<rect className="stroke-border/10" height="180" rx="14.5" strokeWidth="1.5" width="293" x="1.5" y="1.5" />
|
||||
<rect className="fill-page-elevated" height="123" rx="8" width="168" x="112" y="44" />
|
||||
<rect className="stroke-border/5" height="122" rx="7.5" width="167" x="112.5" y="44.5" />
|
||||
<rect className="fill-brand/50" height="16" rx="4" width="64" x="208" y="143" />
|
||||
<path className="stroke-fg-primary" d="M132 64H240" strokeLinecap="round" strokeWidth="5" />
|
||||
<path className="stroke-fg-secondary" d="M132 74H180" strokeLinecap="round" strokeWidth="3" />
|
||||
<path className="stroke-code-error" d="M132 94H216" strokeLinecap="round" strokeWidth="4" />
|
||||
<path className="stroke-code-warn" d="M132 104H192" strokeLinecap="round" strokeWidth="4" />
|
||||
<path className="stroke-code-info" d="M132 114H210" strokeLinecap="round" strokeWidth="4" />
|
||||
<path className="fill-brand" d="M208 147C208 144.791 209.791 143 212 143H240V159H212C209.791 159 208 157.209 208 155V147Z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@ import useCommand from '@/hooks/useCommand'
|
|||
import { bindings } from '@/main'
|
||||
import SettingsRow from '@/components/SettingsRow'
|
||||
import DiscordIcon from "@/assets/logos/discord.svg"
|
||||
import ToggleButton from '@/components/base/ToggleButton'
|
||||
import Switch from '@/components/base/Switch'
|
||||
import Button from '@/components/base/Button'
|
||||
import { FolderIcon, LinkExternal01Icon, XIcon } from '@untitled-theme/icons-react'
|
||||
|
||||
export const Route = createFileRoute('/app/settings/')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -20,14 +21,42 @@ function RouteComponent() {
|
|||
<h1>General Settings</h1>
|
||||
|
||||
<SettingsRow
|
||||
description="Enable Discord Rich Presence."
|
||||
icon={<img src={DiscordIcon} className="w-6 invert-100" />}
|
||||
title="Discord RPC"
|
||||
description="Enable Discord Rich Presence."
|
||||
icon={<img src={DiscordIcon} className="w-6 invert-100" />}
|
||||
title="Discord RPC"
|
||||
>
|
||||
<Switch />
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Hide the confirmation dialog when closing the launcher."
|
||||
icon={<XIcon />}
|
||||
title="Hide Close Dialog"
|
||||
>
|
||||
<Switch />
|
||||
</SettingsRow>
|
||||
|
||||
{/* <SettingsRow
|
||||
description="Sends errors and crash logs using Sentry to help developers fix issues. (// TODO)"
|
||||
icon={<AlertSquareIcon />}
|
||||
title="Error Analytics"
|
||||
>
|
||||
<Switch>Thing</Switch>
|
||||
</SettingsRow>
|
||||
|
||||
<pre>{JSON.stringify(result.data, null, 2)}</pre>
|
||||
<Toggle
|
||||
checked={() => !(settings().disable_analytics ?? false)}
|
||||
onChecked={value => settings().disable_analytics = !value}
|
||||
/>
|
||||
</SettingsRow> */}
|
||||
|
||||
<SettingsRow.Header>Folders and Files</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
description={'Unknown for now'}
|
||||
icon={<FolderIcon />}
|
||||
title="Launcher Folder"
|
||||
>
|
||||
<Button size='normal'>
|
||||
<LinkExternal01Icon /> Open
|
||||
</Button>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from './route'
|
||||
import SettingsRow from '@/components/SettingsRow'
|
||||
import { Maximize01Icon } from '@untitled-theme/icons-react'
|
||||
import { Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react'
|
||||
import Button from '@/components/base/Button'
|
||||
import useCommand from '@/hooks/useCommand'
|
||||
import { bindings } from '@/main'
|
||||
import { TextField } from '@/components/base/TextField'
|
||||
import ToggleButton from '@/components/base/ToggleButton'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import Switch from '@/components/base/Switch'
|
||||
|
||||
export const Route = createFileRoute('/app/settings/minecraft')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -11,19 +17,126 @@ export const Route = createFileRoute('/app/settings/minecraft')({
|
|||
function RouteComponent() {
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<div className="h-full">
|
||||
<h1>Minecraft Settings</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<h1>Minecraft Settings</h1>
|
||||
|
||||
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||
<GameSettings />
|
||||
|
||||
<SettingsRow
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
title="Force Fullscreen"
|
||||
>
|
||||
<Button>something</Button>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
{/* <LauncherSettings /> */}
|
||||
|
||||
<ProcessSettings />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
||||
export function GameSettings() {
|
||||
return <>
|
||||
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
title="Force Fullscreen"
|
||||
>
|
||||
<Switch />
|
||||
</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
|
||||
type='number'
|
||||
className="text-center"
|
||||
/>
|
||||
<XIcon className="size-4 self-center" />
|
||||
<TextField
|
||||
type='number'
|
||||
className="text-center"
|
||||
/>
|
||||
</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">
|
||||
<span>Min:</span>
|
||||
<TextField
|
||||
className="text-center"
|
||||
type='number'
|
||||
max={2024}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<span>Max:</span>
|
||||
<TextField
|
||||
className="text-center"
|
||||
type='number'
|
||||
max={2024}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</>
|
||||
}
|
||||
|
||||
function LauncherSettings() {
|
||||
return <>
|
||||
<SettingsRow.Header>Launcher</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
description="Hide the launcher whenever you start a game."
|
||||
icon={<EyeIcon />}
|
||||
title="Hide On Launch"
|
||||
>
|
||||
<ToggleButton color='primary' children="false" />
|
||||
</SettingsRow>
|
||||
</>
|
||||
}
|
||||
|
||||
export function ProcessSettings() {
|
||||
return <>
|
||||
<SettingsRow.Header>Process</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run before launching the game."
|
||||
icon={<FilePlus02Icon />}
|
||||
title="Pre-Launch Command"
|
||||
>
|
||||
<TextField
|
||||
placeholder="echo 'Game started'"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run when launching the game."
|
||||
icon={<ParagraphWrapIcon />}
|
||||
title="Wrapper Command"
|
||||
>
|
||||
<TextField
|
||||
placeholder="gamescope"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run after exiting the game."
|
||||
icon={<FileX02Icon />}
|
||||
title="Post-Exit Command"
|
||||
>
|
||||
<TextField
|
||||
placeholder="echo 'Game exited'"
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
}
|
||||
26
apps/onelauncher/frontend/src/utils/theming.ts
Normal file
26
apps/onelauncher/frontend/src/utils/theming.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const DEFAULT_THEME = 'OneLauncher-Dark';
|
||||
export const THEMES = {
|
||||
OneLauncher: {
|
||||
variants: [
|
||||
{
|
||||
name: 'Dark',
|
||||
type: 'dark',
|
||||
},
|
||||
{
|
||||
name: 'Light',
|
||||
type: 'light',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as const satisfies Record<string, Theme>;
|
||||
|
||||
export interface Theme {
|
||||
variants: ThemeVariant[];
|
||||
}
|
||||
|
||||
export type ThemeTypes = 'dark' | 'light';
|
||||
|
||||
export interface ThemeVariant {
|
||||
type: ThemeTypes;
|
||||
name: string;
|
||||
};
|
||||
|
|
@ -16,9 +16,16 @@ pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> p
|
|||
proc_macro2::TokenStream::new()
|
||||
};
|
||||
|
||||
let specta_type = quote! {
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
};
|
||||
let specta_type = if let Some(rename) = attrs.rename {
|
||||
quote! {
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[cfg_attr(feature = "specta", specta(rename = #rename))]
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
}
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#specta_event_type
|
||||
|
|
@ -32,12 +39,15 @@ pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> p
|
|||
#[derive(Default)]
|
||||
struct SpectaAttributes {
|
||||
event: bool,
|
||||
rename: Option<String>,
|
||||
}
|
||||
|
||||
impl SpectaAttributes {
|
||||
fn parse(&mut self, meta: ParseNestedMeta) -> syn::Result<()> {
|
||||
if meta.path.is_ident("event") {
|
||||
self.event = true;
|
||||
} else if meta.path.is_ident("rename") {
|
||||
self.rename = Some(meta.value()?.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue