feat: add settings
This commit is contained in:
parent
28050733cb
commit
eebd4e90c2
12 changed files with 376 additions and 75 deletions
|
|
@ -3,6 +3,7 @@ pub mod users;
|
|||
pub mod processor;
|
||||
pub mod package;
|
||||
pub mod other;
|
||||
pub mod settings;
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! collect_commands {
|
||||
|
|
@ -19,6 +20,8 @@ macro_rules! collect_commands {
|
|||
cluster::create_cluster,
|
||||
cluster::get_clusters,
|
||||
|
||||
settings::get_global_profile,
|
||||
|
||||
other::open_dev_tools,
|
||||
]
|
||||
}};
|
||||
|
|
|
|||
10
apps/onelauncher/desktop/src/api/commands/settings.rs
Normal file
10
apps/onelauncher/desktop/src/api/commands/settings.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ function Navbar() {
|
|||
<div className="flex flex-row items-center gap-x-10 py-1">
|
||||
<NavbarLink label="Home" path="/app" />
|
||||
<NavbarLink label="Browser" path="/app/browser" />
|
||||
<NavbarLink label="Settings" path="/app" />
|
||||
<NavbarLink label="Settings" path="/app/settings" />
|
||||
</div>
|
||||
<div className="relative flex flex-row items-center justify-end gap-x-2">
|
||||
<Button color="ghost" size="icon">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
45
apps/onelauncher/frontend/src/components/SettingsRow.tsx
Normal file
45
apps/onelauncher/frontend/src/components/SettingsRow.tsx
Normal 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;
|
||||
30
apps/onelauncher/frontend/src/components/base/Switch.tsx
Normal file
30
apps/onelauncher/frontend/src/components/base/Switch.tsx
Normal 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;
|
||||
|
|
@ -1,33 +1,21 @@
|
|||
import { Dialog, DialogTrigger, Heading, Input, Label, Modal as AriaModal, TextField, ModalOverlay } from 'react-aria-components';
|
||||
import Button from '../base/Button';
|
||||
import type { ClassNameString } from '@/types/global';
|
||||
import {
|
||||
Dialog as AriaDialog,
|
||||
DialogTrigger,
|
||||
Modal as ModalOverlay
|
||||
} from 'react-aria-components';
|
||||
import type { DialogProps } from 'react-aria-components';
|
||||
|
||||
// just testing stuff
|
||||
export function Modal() {
|
||||
return (
|
||||
<>
|
||||
<DialogTrigger>
|
||||
<Button className={"focus:outline-none"}>Slmlar</Button>
|
||||
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<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>
|
||||
<TextField autoFocus>
|
||||
<Label>First Name:</Label>
|
||||
<Input />
|
||||
</TextField>
|
||||
<TextField>
|
||||
<Label>Last Name:</Label>
|
||||
<Input />
|
||||
</TextField>
|
||||
<Button slot="close">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Dialog>
|
||||
</AriaModal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
</>
|
||||
)
|
||||
}
|
||||
interface ModalProps extends DialogProps, ClassNameString { }
|
||||
|
||||
function Modal({ className, ...props }: ModalProps) {
|
||||
return (
|
||||
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.Trigger = DialogTrigger;
|
||||
|
||||
export default Modal;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Button from '@/components/base/Button';
|
||||
import { TextField } from '@/components/base/TextField';
|
||||
import { Modal } from '@/components/overlay/Modal';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/')({
|
||||
|
|
@ -13,7 +14,14 @@ function RouteComponent() {
|
|||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@ import type { Model } from '@/bindings.gen';
|
|||
import Button from '@/components/base/Button';
|
||||
import useCommand from '@/hooks/useCommand';
|
||||
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 { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg"
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
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() {
|
||||
const result = useCommand("getClusters", bindings.commands.getClusters)
|
||||
|
||||
|
|
@ -23,7 +29,7 @@ function RouteComponent() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-96 overflow-y-auto'>
|
||||
<div className='flex flex-col'>
|
||||
<ClusterGroup clusters={result.data} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,6 +108,8 @@ function Banner() {
|
|||
}
|
||||
|
||||
function ClusterCreate() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const result = useCommand("createCluster", () => bindings.commands.createCluster({
|
||||
icon_url: "asd",
|
||||
mc_loader: "vanilla",
|
||||
|
|
@ -121,7 +129,9 @@ function ClusterCreate() {
|
|||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getClusters"],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -143,54 +153,56 @@ function ClusterGroup(props: ClusterGroupProps) {
|
|||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className='flex items-center justify-center h-fit'>
|
||||
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-4 gap-2'>
|
||||
{props.clusters?.map((data) => (
|
||||
<ClusterCard key={data.id} {...data} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<div className="h-full w-full">
|
||||
<OverlayScrollbarsComponent
|
||||
className="h-full w-full"
|
||||
>
|
||||
<div className='grid grid-cols-4 gap-4 max-h-96 2xl:grid-cols-6 pb-4'>
|
||||
{props.clusters?.map((data) => (
|
||||
<ClusterCard key={data.id} {...data} />
|
||||
))}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ClusterCard(props: Model) {
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
>
|
||||
<div className="relative flex-1 overflow-hidden rounded-t-xl">
|
||||
<div
|
||||
className="absolute h-full w-full transition-transform group-hover:!scale-110"
|
||||
>
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
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
|
||||
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="absolute h-full w-full transition-transform group-hover:!scale-110"
|
||||
>
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
src={DefaultInstancePhoto}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
34
apps/onelauncher/frontend/src/routes/app/settings/index.tsx
Normal file
34
apps/onelauncher/frontend/src/routes/app/settings/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
123
apps/onelauncher/frontend/src/routes/app/settings/route.tsx
Normal file
123
apps/onelauncher/frontend/src/routes/app/settings/route.tsx
Normal 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;
|
||||
|
||||
Loading…
Add table
Reference in a new issue