feat: add settings

This commit is contained in:
emirsassan 2025-05-24 19:28:11 +03:00
parent 28050733cb
commit eebd4e90c2
12 changed files with 376 additions and 75 deletions

View file

@ -3,6 +3,7 @@ pub mod users;
pub mod processor; pub mod processor;
pub mod package; pub mod package;
pub mod other; pub mod other;
pub mod settings;
#[macro_export(local_inner_macros)] #[macro_export(local_inner_macros)]
macro_rules! collect_commands { macro_rules! collect_commands {
@ -19,6 +20,8 @@ macro_rules! collect_commands {
cluster::create_cluster, cluster::create_cluster,
cluster::get_clusters, cluster::get_clusters,
settings::get_global_profile,
other::open_dev_tools, other::open_dev_tools,
] ]
}}; }};

View file

@ -0,0 +1,10 @@
use onelauncher_core::entity::setting_profiles::Model;
use onelauncher_core::api::setting_profiles;
use crate::api::error::SerializableResult;
#[specta::specta]
#[tauri::command]
pub async fn get_global_profile() -> SerializableResult<Model> {
Ok(setting_profiles::get_global_profile().await)
}

View file

@ -22,7 +22,7 @@ function Navbar() {
<div className="flex flex-row items-center gap-x-10 py-1"> <div className="flex flex-row items-center gap-x-10 py-1">
<NavbarLink label="Home" path="/app" /> <NavbarLink label="Home" path="/app" />
<NavbarLink label="Browser" path="/app/browser" /> <NavbarLink label="Browser" path="/app/browser" />
<NavbarLink label="Settings" path="/app" /> <NavbarLink label="Settings" path="/app/settings" />
</div> </div>
<div className="relative flex flex-row items-center justify-end gap-x-2"> <div className="relative flex flex-row items-center justify-end gap-x-2">
<Button color="ghost" size="icon"> <Button color="ghost" size="icon">

View file

@ -0,0 +1,19 @@
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
interface ScrollableContainerProps {
children: React.ReactNode;
}
function ScrollableContainer(props: ScrollableContainerProps) {
return (
<div className="h-full h-screen w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
<OverlayScrollbarsComponent className="os-hide-horizontal-scrollbar relative flex-1">
<div className="absolute h-full w-[calc(100%-14px)] flex flex-col gap-2">
{props.children}
</div>
</OverlayScrollbarsComponent>
</div>
);
}
export default ScrollableContainer;

View file

@ -0,0 +1,45 @@
import type { JSX } from "react";
export type SettingsRowProps = {
title: JSX.Element | string;
description: JSX.Element | string;
icon: JSX.Element;
disabled?: boolean;
children?: JSX.Element;
};
function SettingsRow(props: SettingsRowProps) {
return (
<div
className={`
flex flex-row items-center gap-3.5 rounded-xl p-4
${props.disabled ? 'bg-component-bg-disabled' : 'bg-page-elevated hover:bg-component-bg-hover'}
`}
>
<div className="flex h-8 w-8 items-center justify-center">
{props.icon}
</div>
<div className="flex flex-1 flex-col gap-2">
<h3 className="text-lg capitalize leading-tight">{props.title}</h3>
<p className="text-sm text-fg-secondary">{props.description}</p>
</div>
<div className="flex h-9 flex-row items-center gap-2">
{props.children}
</div>
</div>
);
}
interface SettingsRowHeaderProps {
className?: string;
children?: React.ReactNode;
}
SettingsRow.Header = ({ className, children }: SettingsRowHeaderProps) => {
const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`;
return <h3 className={headerClasses}>{children}</h3>;
};
export default SettingsRow;

View file

@ -0,0 +1,30 @@
import type React from 'react';
import type { RefAttributes } from 'react';
import type { SwitchProps as AriaSwitchProps } from 'react-aria-components';
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',
],
})
export type SwitchProps = AriaSwitchProps & RefAttributes<HTMLLabelElement> & {
className?: string;
children?: React.ReactNode;
};
function Switch({
className,
...props
}: SwitchProps) {
return (
<AriaSwitch className={switchVariants({ className })} {...props}>
<div className="bg-orange-400" />
{props.children}
</AriaSwitch>
);
}
export default Switch;

View file

@ -1,33 +1,21 @@
import { Dialog, DialogTrigger, Heading, Input, Label, Modal as AriaModal, TextField, ModalOverlay } from 'react-aria-components'; import type { ClassNameString } from '@/types/global';
import Button from '../base/Button'; import {
Dialog as AriaDialog,
DialogTrigger,
Modal as ModalOverlay
} from 'react-aria-components';
import type { DialogProps } from 'react-aria-components';
// just testing stuff interface ModalProps extends DialogProps, ClassNameString { }
export function Modal() {
return ( function Modal({ className, ...props }: ModalProps) {
<> return (
<DialogTrigger> <ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<Button className={"focus:outline-none"}>Slmlar</Button> <AriaDialog className="min-w-md flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center focus:outline-none" {...props} />
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> </ModalOverlay>
<AriaModal> );
<Dialog className="min-w-md flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center focus:outline-none"> }
<form>
<Heading slot="title">Sign up</Heading> Modal.Trigger = DialogTrigger;
<TextField autoFocus>
<Label>First Name:</Label> export default Modal;
<Input />
</TextField>
<TextField>
<Label>Last Name:</Label>
<Input />
</TextField>
<Button slot="close">
Submit
</Button>
</form>
</Dialog>
</AriaModal>
</ModalOverlay>
</DialogTrigger>
</>
)
}

View file

@ -1,5 +1,6 @@
import Button from '@/components/base/Button';
import { TextField } from '@/components/base/TextField'; import { TextField } from '@/components/base/TextField';
import { Modal } from '@/components/overlay/Modal'; import Modal from '@/components/overlay/Modal';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/browser/')({ export const Route = createFileRoute('/app/browser/')({
@ -13,7 +14,14 @@ function RouteComponent() {
<TextField /> <TextField />
<Modal /> {/* we can open the modal but we cant close it for some reason */}
<Modal.Trigger>
<Button>Open Modal</Button>
<Modal>
<p>sadsadsad</p>
</Modal>
</Modal.Trigger>
</div> </div>
); );
} }

View file

@ -3,13 +3,19 @@ import type { Model } from '@/bindings.gen';
import Button from '@/components/base/Button'; import Button from '@/components/base/Button';
import useCommand from '@/hooks/useCommand'; import useCommand from '@/hooks/useCommand';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { useIsFetching } from '@tanstack/react-query'; import { useIsFetching, useQueryClient } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg"
export const Route = createFileRoute('/app/')({ export const Route = createFileRoute('/app/')({
component: RouteComponent, component: RouteComponent,
}); });
/*
Please note this route has a very big issue related to scrolling
and i am very angry rn so i will not be fixing it rn
*/
function RouteComponent() { function RouteComponent() {
const result = useCommand("getClusters", bindings.commands.getClusters) const result = useCommand("getClusters", bindings.commands.getClusters)
@ -23,7 +29,7 @@ function RouteComponent() {
</div> </div>
</div> </div>
<div className='h-96 overflow-y-auto'> <div className='flex flex-col'>
<ClusterGroup clusters={result.data} /> <ClusterGroup clusters={result.data} />
</div> </div>
</div> </div>
@ -102,6 +108,8 @@ function Banner() {
} }
function ClusterCreate() { function ClusterCreate() {
const queryClient = useQueryClient();
const result = useCommand("createCluster", () => bindings.commands.createCluster({ const result = useCommand("createCluster", () => bindings.commands.createCluster({
icon_url: "asd", icon_url: "asd",
mc_loader: "vanilla", mc_loader: "vanilla",
@ -121,7 +129,9 @@ function ClusterCreate() {
return; return;
} }
return; queryClient.invalidateQueries({
queryKey: ["getClusters"],
})
} }
return ( return (
@ -143,54 +153,56 @@ function ClusterGroup(props: ClusterGroupProps) {
if (isFetching) { if (isFetching) {
return ( return (
<div className='flex items-center justify-center h-fit'> <div className='flex items-center justify-center h-full'>
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" /> <div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
</div> </div>
) )
} }
return ( return (
<> <div className="h-full w-full">
<div className='grid grid-cols-4 gap-2'> <OverlayScrollbarsComponent
{props.clusters?.map((data) => ( className="h-full w-full"
<ClusterCard key={data.id} {...data} /> >
))} <div className='grid grid-cols-4 gap-4 max-h-96 2xl:grid-cols-6 pb-4'>
</div> {props.clusters?.map((data) => (
</> <ClusterCard key={data.id} {...data} />
))}
</div>
</OverlayScrollbarsComponent>
</div>
) )
} }
function ClusterCard(props: Model) { function ClusterCard(props: Model) {
return ( return (
<> <div
<div className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover" >
> <div className="relative flex-1 overflow-hidden rounded-t-xl">
<div className="relative flex-1 overflow-hidden rounded-t-xl"> <div
<div className="absolute h-full w-full transition-transform group-hover:!scale-110"
className="absolute h-full w-full transition-transform group-hover:!scale-110" >
> <img
<img className="h-full w-full object-cover"
className="h-full w-full object-cover" src={DefaultInstancePhoto}
src='https://github.com/emirsassan.png' />
/>
</div>
</div>
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{props.name}</p>
<p className="h-4 text-xs">
{props.mc_loader}
{' '}
{props.mc_version}
{/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>
</div>
{/* <LaunchButton cluster={props} iconOnly /> */}
</div> </div>
</div> </div>
</> <div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{props.name}</p>
<p className="h-4 text-xs">
{props.mc_loader}
{' '}
{props.mc_version}
{/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>
</div>
{/* <LaunchButton cluster={props} iconOnly /> */}
</div>
</div>
); );
} }

View file

@ -0,0 +1,34 @@
import { createFileRoute } from '@tanstack/react-router'
import Sidebar from './route'
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'
export const Route = createFileRoute('/app/settings/')({
component: RouteComponent,
})
function RouteComponent() {
const result = useCommand("getGlobalProfile", bindings.commands.getGlobalProfile)
return (
<Sidebar.Page>
<div>
<h1>General Settings</h1>
<SettingsRow
description="Enable Discord Rich Presence."
icon={<img src={DiscordIcon} className="w-6 invert-100" />}
title="Discord RPC"
>
<Switch>Thing</Switch>
</SettingsRow>
<pre>{JSON.stringify(result.data, null, 2)}</pre>
</div>
</Sidebar.Page>
)
}

View file

@ -0,0 +1,29 @@
import { createFileRoute } from '@tanstack/react-router'
import Sidebar from './route'
import SettingsRow from '@/components/SettingsRow'
import { Maximize01Icon } from '@untitled-theme/icons-react'
import Button from '@/components/base/Button'
export const Route = createFileRoute('/app/settings/minecraft')({
component: RouteComponent,
})
function RouteComponent() {
return (
<Sidebar.Page>
<div>
<h1>Minecraft Settings</h1>
<SettingsRow.Header>Game</SettingsRow.Header>
<SettingsRow
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
title="Force Fullscreen"
>
<Button>something</Button>
</SettingsRow>
</div>
</Sidebar.Page>
)
}

View file

@ -0,0 +1,123 @@
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'
import { Brush01Icon, CodeSnippet02Icon, MessageTextSquare01Icon, RefreshCcw02Icon, Rocket02Icon, Sliders04Icon, Users01Icon } from '@untitled-theme/icons-react';
import { useEffect, type JSX } from 'react';
export const Route = createFileRoute('/app/settings')({
component: RouteComponent,
})
function RouteComponent() {
return (
<div className="h-full flex flex-1 flex-row gap-x-7">
<div className="mt-8 flex flex-col justify-between">
<Sidebar
base="/app/settings"
links={{
'Launcher Settings': [
[<Rocket02Icon />, 'General', '/'],
[<Brush01Icon />, 'Appearance', '/appearance'],
// [<Key01Icon />, 'APIs', '/apis'],
// [<Globe01Icon />, 'Language', '/language'],
],
'Game Settings': [
[<Sliders04Icon />, 'Minecraft settings', '/minecraft'],
[<Users01Icon />, 'Accounts', '/accounts'],
],
'About': [
[<RefreshCcw02Icon />, 'Changelog', '/changelog'],
[<MessageTextSquare01Icon />, 'Feedback', '/feedback'],
[<CodeSnippet02Icon />, 'Developer Options', '/developer'],
],
}}
/>
{/* <Info /> */}
</div>
<div className="h-full w-full flex flex-col">
<Outlet />
</div>
</div>
)
}
type SidebarProps = {
base: string;
links: Record<string, ([JSX.Element, string, string, URLSearchParams?] | undefined)[]>;
};
function Sidebar(props: SidebarProps) {
const navigate = useNavigate();
const routerState = useRouterState();
const location = routerState.location.pathname
useEffect(() => {
if (props.base.endsWith('/'))
throw new Error('Base should not end with a slash');
});
function goto(href: string, params?: URLSearchParams) {
const currParams = new URLSearchParams(location);
if (params)
for (const [key, value] of params)
currParams.set(key, value);
const url = `${props.base}${href}`;
navigate({ to: url })
}
function isActive(link: string, params: URLSearchParams | undefined) {
const pathnameMatch = location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`;
const paramsMatch = params ? location.endsWith(params.toString()) : true;
return pathnameMatch && paramsMatch;
}
return (
<div className="w-56 flex flex-col pr-2">
{Object.keys(props.links).map((section, i) => (
<div className="flex flex-col gap-y-2">
<div key={i}>
<h3 className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</h3>
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
{props.links[section].map((link) => {
if (!link)
return;
return (
<a
className={
`px-3 py-1 rounded-md text-md border border-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg border-border/05' : 'border-transparent'}`
}
onClick={() => goto(link[2], link[3])}
>
<span className="flex flex-row items-center gap-x-3 *:w-5">
{link[0]}
{link[1]}
</span>
</a>
)
})}
</div>
</div>
</div>
)
)}
</div>
);
}
interface SidebarPageProps {
className?: string;
children: JSX.Element;
}
Sidebar.Page = function (props: SidebarPageProps) {
return (
<div {...props} className={`flex flex-col flex-1 gap-y-1 ${props.className || ''}`}>
{props.children}
</div>
);
};
export default Sidebar;