diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 4692aaac..35fb9148 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -230,7 +230,7 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'core':'{"getClusterById":["id"],"getLogByName":["id","name"],"getLogs":["id"],"openMsaLogin":[],"writeSettings":["setting"],"removeUser":["uuid"],"searchPackages":["provider","query"],"getClusters":[],"updateClusterById":["id","request"],"getUser":["uuid"],"getPackage":["provider","slug"],"getLoadersForVersion":["mc_version"],"getUsers":[],"getScreenshots":["id"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"updateClusterProfile":["name","profile"],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"getWorlds":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsersFromAuthor":["provider","author"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"readSettings":[],"getGlobalProfile":[],"getDefaultUser":["fallback"],"getGameVersions":[],"createCluster":["options"],"getProfileOrDefault":["name"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' } +const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'core':'{"readSettings":[],"getLogs":["id"],"getGameVersions":[],"getClusters":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"launchCluster":["id","uuid"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"updateClusterProfile":["name","profile"],"openMsaLogin":[],"getDefaultUser":["fallback"],"getScreenshots":["id"],"removeCluster":["id"],"getMultiplePackages":["provider","slugs"],"writeSettings":["setting"],"getUsers":[],"createCluster":["options"],"getWorlds":["id"],"updateClusterById":["id","request"],"getGlobalProfile":[],"getUser":["uuid"],"getLogByName":["id","name"],"searchPackages":["provider","query"],"getProfileOrDefault":["name"],"getLoadersForVersion":["mc_version"],"getPackage":["provider","slug"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchMinecraftProfile":["uuid"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}' } export type Router = { 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise> }, 'core': { getClusters: () => Promise, diff --git a/apps/oneclient/frontend/src/components/Navbar.tsx b/apps/oneclient/frontend/src/components/Navbar.tsx index de0d3734..f09af13c 100644 --- a/apps/oneclient/frontend/src/components/Navbar.tsx +++ b/apps/oneclient/frontend/src/components/Navbar.tsx @@ -4,15 +4,16 @@ import LauncherLogo from '@/assets/logos/oneclient.svg?react'; import { bindings } from '@/main'; import { useCommand } from '@onelauncher/common'; import { Button, Popup } from '@onelauncher/common/components'; -import { Link } from '@tanstack/react-router'; +import { Link, useNavigate } from '@tanstack/react-router'; import { Window } from '@tauri-apps/api/window'; -import { MinusIcon, SquareIcon, XCloseIcon } from '@untitled-theme/icons-react'; +import { MinusIcon, Settings01Icon, Settings02Icon, SquareIcon, XCloseIcon } from '@untitled-theme/icons-react'; import { twMerge } from 'tailwind-merge'; import { AccountAvatar } from './AccountAvatar'; import { AccountPopup } from './overlay/AccountPopup'; export function Navbar() { const { data: currentAccount } = useCommand(['getDefaultUser'], () => bindings.core.getDefaultUser(true)); + const navigate = useNavigate(); const onMinimize = () => Window.getCurrent().minimize(); const onMaximize = () => Window.getCurrent().toggleMaximize(); @@ -31,6 +32,10 @@ export function Navbar() {
+ } + onClick={() => navigate({ to: '/app/settings' })} + /> diff --git a/apps/oneclient/frontend/src/components/PackageItem.tsx b/apps/oneclient/frontend/src/components/PackageItem.tsx index 0ff0fe49..cf8b1b5d 100644 --- a/apps/oneclient/frontend/src/components/PackageItem.tsx +++ b/apps/oneclient/frontend/src/components/PackageItem.tsx @@ -21,6 +21,7 @@ export function PackageItem({ provider, clusterId, ...item }: SearchResult & { p return ( void, +]; + +export default function SettingsSwitch({ setting }: { setting: BooleanSetting }) { + return ( + setting[1](val)} + /> + ); +} diff --git a/apps/oneclient/frontend/src/components/SettingsRow.tsx b/apps/oneclient/frontend/src/components/SettingsRow.tsx new file mode 100644 index 00000000..4bfe7435 --- /dev/null +++ b/apps/oneclient/frontend/src/components/SettingsRow.tsx @@ -0,0 +1,45 @@ +import type { JSX } from 'react'; + +export interface SettingsRowProps { + title: JSX.Element | string; + description: JSX.Element | string; + icon: JSX.Element; + disabled?: boolean; + children?: JSX.Element; +} + +function SettingsRow({ disabled, description, icon, title, children }: SettingsRowProps) { + return ( +
+
+ {icon} +
+ +
+

{title}

+

{description}

+
+ +
+ {children} +
+
+ ); +} + +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

{children}

; +}; + +export default SettingsRow; diff --git a/apps/oneclient/frontend/src/hooks/usePopState.ts b/apps/oneclient/frontend/src/hooks/usePopState.ts new file mode 100644 index 00000000..5714b47c --- /dev/null +++ b/apps/oneclient/frontend/src/hooks/usePopState.ts @@ -0,0 +1,20 @@ +import { useLocation } from '@tanstack/react-router'; +import { useEffect, useRef } from 'react'; + +export default function usePopState(onRouteChange: void | any) { + const location = useLocation(); + const prevLocationRef = useRef(location); + + useEffect(() => { + const prevLocation = prevLocationRef.current; + + // Only run if the pathname actually changed + if (prevLocation.pathname !== location.pathname) + onRouteChange?.({ + from: prevLocation, + to: location, + }); + + prevLocationRef.current = location; + }, [location, onRouteChange]); +} diff --git a/apps/oneclient/frontend/src/hooks/useSettings.ts b/apps/oneclient/frontend/src/hooks/useSettings.ts new file mode 100644 index 00000000..a8a2d315 --- /dev/null +++ b/apps/oneclient/frontend/src/hooks/useSettings.ts @@ -0,0 +1,25 @@ +import type { Settings } from '@/bindings.gen'; +import { bindings } from '@/main'; +import { useCommandSuspense } from '@onelauncher/common'; +import { useMemo } from 'react'; + +export function useSettings() { + const settings = useCommandSuspense(['readSettings'], bindings.core.readSettings); + const setting = useMemo(() => (key: TKey) => settings.data[key], [settings]); + + const setSetting = useMemo(() => (key: TKey, value: Settings[TKey]) => { + bindings.core.writeSettings({ + ...settings.data, + [key]: value, + }); + settings.refetch(); + }, [settings]); + + type Setter = (value: Settings[TKey]) => void; + + const createSetting = useMemo(() => (key: TKey): [Settings[TKey], Setter] => { + return [setting(key), value => setSetting(key, value)]; + }, [setting]); + + return { setting, setSetting, createSetting }; +} diff --git a/apps/oneclient/frontend/src/routes/app/settings/appearance.tsx b/apps/oneclient/frontend/src/routes/app/settings/appearance.tsx new file mode 100644 index 00000000..6b83f19e --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/settings/appearance.tsx @@ -0,0 +1,224 @@ +// import ScrollableContainer from '@/components/ScrollableContainer'; +import SettingsRow from '@/components/SettingsRow'; +import SettingsSwitch from '@/components/SettingSwitch'; +import { useSettings } from '@/hooks/useSettings'; +import { bindings } from '@/main'; +import { THEMES } from '@/utils/theming'; +import { Switch } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { Monitor01Icon, PackageIcon, Speedometer04Icon } from '@untitled-theme/icons-react'; +import Sidebar from './route'; + +export const Route = createFileRoute('/app/settings/appearance')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { createSetting } = useSettings(); + + return ( + + {/* */} +
+

Appearance

+ +
+ +
+ {/* + {theme => ( + + {variant => ( + + )} + + )} + */} + + {Object.keys(THEMES).map((theme) => { + return THEMES[theme as keyof typeof THEMES].variants.map((variant) => { + return ( + { }} themeSelector={`${theme}-${variant.name}`} /> + ); + }); + })} +
+
+ + } + title="Package List Style" + > +

i hope i'll fix this

+
+ + } + title="Custom Window Frame" + > + + + + } + title="Disable Animations" + > + {/* */} + +
+ {/*
*/} +
+ ); +} + +interface ThemeCardProps { + themeSelector: string; + setTheme: (merged: string) => void; +}; + +function ThemeCard({ + themeSelector, + setTheme, +}: ThemeCardProps) { + return ( +
setTheme(themeSelector)}> + + + + + + + + + +
+ ); +} + +function PrimaryThemeCard() { + return ( +
+ + + + + + + + + + + + + +
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/settings/index.tsx b/apps/oneclient/frontend/src/routes/app/settings/index.tsx new file mode 100644 index 00000000..364cebfe --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/settings/index.tsx @@ -0,0 +1,81 @@ +// import DiscordIcon from '@/assets/logos/discord.svg'; +import SettingsRow from '@/components/SettingsRow'; +import { useSettings } from '@/hooks/useSettings'; +// import useSettings from '@/hooks/useSettings'; +import { Button, Switch } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { dataDir, join } from '@tauri-apps/api/path'; +import { openPath } from '@tauri-apps/plugin-opener'; +import { FolderIcon, Link03Icon, LinkExternal01Icon } from '@untitled-theme/icons-react'; +import { useEffect, useState } from 'react'; +import Sidebar from './route'; + +export const Route = createFileRoute('/app/settings/')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { setting, setSetting } = useSettings(); + const [launcherDir, setLauncherDir] = useState(''); + + useEffect(() => { + (async () => { + setLauncherDir(await join(await dataDir(), 'OneLauncher')); + })(); + }, []); + + const openLauncherDir = () => openPath(launcherDir); + + // const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled); + + return ( + +
+

General

+ + } + title="Discord RPC" + > + setSetting('discord_enabled', val)} + /> + + + {/* } + title="Hide Close Dialog" + > + + + + } + title="Error Analytics" + > + !(settings().disable_analytics ?? false)} + onChecked={value => settings().disable_analytics = !value} + /> + */} + + Folders and Files + } + title="Launcher Folder" + > + + +
+
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/settings/minecraft.tsx b/apps/oneclient/frontend/src/routes/app/settings/minecraft.tsx new file mode 100644 index 00000000..fc0358ca --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/settings/minecraft.tsx @@ -0,0 +1,191 @@ +import type { SettingProfileModel } from '@/bindings.gen'; +// import ScrollableContainer from '@/components/ScrollableContainer'; +import SettingsRow from '@/components/SettingsRow'; +import usePopState from '@/hooks/usePopState'; +import { useSettings } from '@/hooks/useSettings'; +import { bindings } from '@/main'; +import { useCommandMut, useCommandSuspense } from '@onelauncher/common'; +import { Switch, TextField } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react'; +import { useMemo } from 'react'; +import Sidebar from './route'; + +export const Route = createFileRoute('/app/settings/minecraft')({ + component: RouteComponent, +}); + +function RouteComponent() { + const result = useCommandSuspense(['readSettings'], bindings.core.readSettings); + + const save = useCommandMut(() => bindings.core.writeSettings(result.data)); + + usePopState(() => { + save.mutate(); + }); + + return ( + + {/* */} +
+

Minecraft Settings

+ + + + {/* */} + + +
+ {/*
*/} +
+ ); +} + +export function GameSettings() { + const { createSetting } = useSettings(); + + const [gameSettings, setGameSettings] = createSetting('global_game_settings'); + + // const getSetting = useMemo(()=>(key: TKey)=>gameSettings[key], [gameSettings]) + + const setSetting = useMemo(() => (key: TKey, value: SettingProfileModel[TKey]) => { + setGameSettings({ + ...gameSettings, + [key]: value, + }); + }, [gameSettings, setGameSettings]); + + return ( + <> + Game + + } + title="Force Fullscreen" + > + setSetting('force_fullscreen', val)} + /> + + + } + title="Resolution" + > +
+ { + setSetting('res', { height: gameSettings.res?.height ?? 720, width: Number(e.currentTarget.value) }); + }} + type="number" + value={gameSettings.res?.width} + /> + + { + setSetting('res', { width: gameSettings.res?.height ?? 1280, height: Number(e.currentTarget.value) }); + }} + type="number" + value={gameSettings.res?.height} + /> +
+
+ + } + title="Memory" + > +
+
+ { + setSetting('mem_max', Number(e.currentTarget.value)); + }} + type="number" + value={gameSettings.mem_max ?? undefined} + /> +
+
+
+ + ); +} + +// function LauncherSettings() { +// return ( +// <> +// Launcher +// } +// title="Hide On Launch" +// > +// +// +// +// ); +// } + +export function ProcessSettings() { + const { createSetting } = useSettings(); + + const [gameSettings, setGameSettings] = createSetting('global_game_settings'); + + // const getSetting = useMemo(()=>(key: TKey)=>gameSettings[key], [gameSettings]) + + const setSetting = useMemo(() => (key: TKey, value: SettingProfileModel[TKey]) => { + setGameSettings({ + ...gameSettings, + [key]: value, + }); + }, [gameSettings, setGameSettings]); + + return ( + <> + Process + + } + title="Pre-Launch Command" + > + setSetting('hook_pre', e.currentTarget.value)} + placeholder="echo 'Game started'" + value={gameSettings.hook_pre ?? undefined} + /> + + + } + title="Wrapper Command" + > + setSetting('hook_wrapper', e.currentTarget.value)} + placeholder="gamescope" + value={gameSettings.hook_wrapper ?? undefined} + /> + + + } + title="Post-Exit Command" + > + setSetting('hook_post', e.currentTarget.value)} + placeholder="echo 'Game exited'" + value={gameSettings.hook_post ?? undefined} + /> + + + ); +} diff --git a/apps/oneclient/frontend/src/routes/app/settings/route.tsx b/apps/oneclient/frontend/src/routes/app/settings/route.tsx new file mode 100644 index 00000000..b51ab5ba --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/settings/route.tsx @@ -0,0 +1,135 @@ +import type { JSX } from 'react'; +import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; +import { Brush01Icon, CodeSnippet02Icon, MessageTextSquare01Icon, RefreshCcw02Icon, Rocket02Icon, Sliders04Icon, Users01Icon } from '@untitled-theme/icons-react'; +import { useEffect } from 'react'; + +export const Route = createFileRoute('/app/settings')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( +
+
+ , 'General', '/'], + [, 'Appearance', '/appearance'], + // [, 'APIs', '/apis'], + // [, 'Language', '/language'], + ], + 'Game Settings': [ + [, 'Minecraft settings', '/minecraft'], + ], + 'About': [ + [, 'Changelog', '/changelog'], + [, 'Feedback', '/feedback'], + [, 'Developer Options', '/developer'], + ], + }} + /> + {/* */} +
+ +
+ +
+
+ ); +} + +interface SidebarProps { + base: string; + links: Record>; +} + +function Sidebar(props: SidebarProps) { + const { base, links } = props; + + const navigate = useNavigate(); + const routerState = useRouterState(); + + const location = routerState.location.pathname; + + useEffect(() => { + if (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 = `${base}${href}`; + navigate({ to: url }); + } + + function isActive(link: string, params: URLSearchParams | undefined) { + const fullBasePath = `${base}${link}`; + const pathnameMatch = location.startsWith(fullBasePath) || `${location}/`.startsWith(fullBasePath); + + if (!params) + return pathnameMatch; + + const searchParams = new URLSearchParams(routerState.location.searchStr); + for (const [key, value] of params) + if (searchParams.get(key) !== value) + return false; + + return pathnameMatch; + } + + return ( +
+ {Object.keys(links).map(section => ( +
+
+

{section.toUpperCase()}

+
+ {links[section].map((entry) => { + if (!entry) + return ''; + + const [Component, label, link, params] = entry; + + return ( + goto(link, params)} + > + + {Component} + {label} + + + ); + })} +
+
+
+ ))} +
+ ); +} + +interface SidebarPageProps { + className?: string; + children: JSX.Element | Array; +} + +Sidebar.Page = function (props: SidebarPageProps) { + return ( +
+ {props.children} +
+ ); +}; + +export default Sidebar;