Merge branch 'oneclient/main' of https://github.com/Polyfrost/OneLauncher into oneclient/main
This commit is contained in:
commit
3c9ff82314
19 changed files with 896 additions and 107 deletions
|
|
@ -232,9 +232,20 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
<<<<<<< HEAD
|
||||
const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getRunningProcessesByClusterId":["cluster_id"],"writeSettings":["setting"],"getPackage":["provider","slug"],"open":["input"],"updateClusterProfile":["name","profile"],"getScreenshots":["id"],"fetchMinecraftProfile":["uuid"],"launchCluster":["id","uuid"],"readSettings":[],"getMultiplePackages":["provider","slugs"],"getClusters":[],"updateClusterById":["id","request"],"getRunningProcesses":[],"getGlobalProfile":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProfileOrDefault":["name"],"getUser":["uuid"],"createCluster":["options"],"killProcess":["pid"],"setDefaultUser":["uuid"],"openMsaLogin":[],"searchPackages":["provider","query"],"isClusterRunning":["cluster_id"],"getPackageBody":["provider","body"],"getLoadersForVersion":["mc_version"],"removeUser":["uuid"],"getClusterById":["id"],"removeCluster":["id"],"getGameVersions":[],"getUsers":[],"getUsersFromAuthor":["provider","author"],"getLogByName":["id","name"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getDefaultUser":["fallback"],"getLogs":["id"],"getWorlds":["id"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}' }
|
||||
export type Router = { 'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
=======
|
||||
const ARGS_MAP = { 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}', 'core':'{"writeSettings":["setting"],"getWorlds":["id"],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getClusterById":["id"],"openMsaLogin":[],"getScreenshots":["id"],"getUsers":[],"getProfileOrDefault":["name"],"updateClusterById":["id","request"],"getDefaultUser":["fallback"],"removeCluster":["id"],"createCluster":["options"],"getPackage":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"open":["input"],"getMultiplePackages":["provider","slugs"],"getLogByName":["id","name"],"getLoadersForVersion":["mc_version"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"setDefaultUser":["uuid"],"getUser":["uuid"],"killProcess":["pid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"launchCluster":["id","uuid"],"getClusters":[],"getRunningProcesses":[],"getGameVersions":[],"getGlobalProfile":[],"getPackageBody":["provider","body"],"removeUser":["uuid"],"fetchMinecraftProfile":["uuid"],"readSettings":[],"getLogs":["id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' }
|
||||
export type Router = { 'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
>>>>>>> 035657cb791987107fa8d13da125fbb9d6d60825
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
|
|
@ -270,12 +281,16 @@ getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null,
|
|||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
|
||||
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
|
||||
<<<<<<< HEAD
|
||||
open: (input: string) => Promise<null> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
=======
|
||||
open: (input: string) => Promise<null> } };
|
||||
>>>>>>> 035657cb791987107fa8d13da125fbb9d6d60825
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function ExternalLink({
|
|||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{withIcon && <LinkExternal01Icon className="inline w-4 ml-1" />}
|
||||
{withIcon && <LinkExternal01Icon className="inline w-3 ml-0.5 h-3" />}
|
||||
</a>
|
||||
|
||||
<Overlay>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -30,37 +31,42 @@ export function Navbar() {
|
|||
<NavbarLink to="./accounts">Accounts</NavbarLink>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-end gap-2 pointer-events-none">
|
||||
<Popup.Trigger>
|
||||
<NavbarButton>
|
||||
<AccountAvatar className="w-full h-full rounded-lg" uuid={currentAccount?.id} />
|
||||
</NavbarButton>
|
||||
<div className="flex flex-1 items-center justify-end gap-6 pointer-events-none">
|
||||
<div className="flex gap-4">
|
||||
<NavbarButton
|
||||
children={<Settings02Icon height={28} width={28} />}
|
||||
onClick={() => navigate({ to: '/app/settings' })}
|
||||
/>
|
||||
<Popup.Trigger>
|
||||
<NavbarButton>
|
||||
<AccountAvatar className="w-full h-full rounded-lg" uuid={currentAccount?.id} />
|
||||
</NavbarButton>
|
||||
<AccountPopup />
|
||||
</Popup.Trigger>
|
||||
</div>
|
||||
|
||||
<AccountPopup />
|
||||
</Popup.Trigger>
|
||||
|
||||
<NavbarButton
|
||||
children={<MinusIcon />}
|
||||
onClick={onMinimize}
|
||||
/>
|
||||
|
||||
<NavbarButton
|
||||
children={<SquareIcon />}
|
||||
onClick={onMaximize}
|
||||
/>
|
||||
|
||||
<NavbarButton
|
||||
children={(
|
||||
<XCloseIcon
|
||||
height={28}
|
||||
strokeWidth={1.5}
|
||||
width={28}
|
||||
/>
|
||||
)}
|
||||
className="bg-transparent"
|
||||
color="danger"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<NavbarButton
|
||||
children={<MinusIcon />}
|
||||
onClick={onMinimize}
|
||||
/>
|
||||
<NavbarButton
|
||||
children={<SquareIcon />}
|
||||
onClick={onMaximize}
|
||||
/>
|
||||
<NavbarButton
|
||||
children={(
|
||||
<XCloseIcon
|
||||
height={28}
|
||||
strokeWidth={1.5}
|
||||
width={28}
|
||||
/>
|
||||
)}
|
||||
className="bg-transparent"
|
||||
color="danger"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
|
|
|||
16
apps/oneclient/frontend/src/components/SettingSwitch.tsx
Normal file
16
apps/oneclient/frontend/src/components/SettingSwitch.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { useSettings } from '@/hooks/useSettings';
|
||||
import { Switch } from '@onelauncher/common/components';
|
||||
|
||||
type BooleanSetting = [
|
||||
boolean,
|
||||
(value: boolean) => void,
|
||||
];
|
||||
|
||||
export default function SettingsSwitch({ setting }: { setting: BooleanSetting }) {
|
||||
return (
|
||||
<Switch
|
||||
isSelected={setting[0]}
|
||||
onChange={val => setting[1](val)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
45
apps/oneclient/frontend/src/components/SettingsRow.tsx
Normal file
45
apps/oneclient/frontend/src/components/SettingsRow.tsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
className={`
|
||||
flex flex-row items-center gap-3.5 rounded-xl p-4 my-2
|
||||
${disabled ? 'bg-component-bg-disabled' : 'bg-page-elevated hover:bg-component-bg-hover'}
|
||||
`}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<p className="text-lg capitalize leading-tight">{title}</p>
|
||||
<p className="text-sm text-fg-secondary">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex h-9 flex-row items-center gap-2">
|
||||
{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;
|
||||
|
|
@ -15,7 +15,7 @@ export function TabList({
|
|||
<div className="pointer-events-none sticky top-0 z-10 min-h-[74px] h-[74px] max-h-[74px] w-full" ref={ref}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'pointer-events-auto flex flex-row gap-2 border border-transparent bg-page-elevated transition-all',
|
||||
'pointer-events-auto flex flex-row gap-4 border border-transparent bg-page-elevated transition-all',
|
||||
floating
|
||||
? 'px-6 mx-4 py-3 shadow-lg border-ghost-overlay rounded-xl'
|
||||
: 'px-10 py-6 rounded-2xl',
|
||||
|
|
|
|||
|
|
@ -1,76 +1,99 @@
|
|||
import type { MinecraftCredentials } from '@/bindings.gen';
|
||||
import { AccountAvatar } from '@/components/AccountAvatar';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommandMut, useCommandSuspense } from '@onelauncher/common';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Button as AriaButton } from 'react-aria-components';
|
||||
import { useCommand, useCommandMut } from '@onelauncher/common';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { PlusIcon, Settings01Icon } from '@untitled-theme/icons-react';
|
||||
import { DialogTrigger } from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { AccountAvatar } from '../AccountAvatar';
|
||||
import { AddAccountModal } from './AddAccountModal';
|
||||
import { Overlay } from './Overlay';
|
||||
import { Popup } from './Popup';
|
||||
|
||||
export function AccountPopup() {
|
||||
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
|
||||
const { data: accounts } = useCommandSuspense(['getUsers', 'accountPopup'], bindings.core.getUsers, {
|
||||
select(data) {
|
||||
data.sort((a, b) => {
|
||||
if (currentAccount?.id === a.id)
|
||||
return -1;
|
||||
else if (currentAccount?.id === b.id)
|
||||
return 1;
|
||||
return a.username.localeCompare(b.username);
|
||||
});
|
||||
const users = useCommand(['getUsers'], bindings.core.getUsers);
|
||||
const defaultUser = useCommand(['getDefaultUser'], () => bindings.core.getDefaultUser(false));
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: setDefaultUser } = useCommandMut(bindings.core.setDefaultUser, {
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['getDefaultUser'],
|
||||
});
|
||||
},
|
||||
});
|
||||
const setDefaultUser = (user: MinecraftCredentials) => {
|
||||
bindings.core.setDefaultUser(user.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popup placement="top left">
|
||||
<div className="flex flex-col gap-1 min-w-40">
|
||||
{accounts.map(profile => (
|
||||
<AccountRow
|
||||
key={profile.id}
|
||||
onPress={() => setDefaultUser(profile.id)}
|
||||
profile={profile}
|
||||
selected={profile.id === currentAccount?.id}
|
||||
/>
|
||||
|
||||
<div className="min-w-3xs">
|
||||
{defaultUser.data && (
|
||||
<div>
|
||||
<AccountEntry
|
||||
loggedIn
|
||||
onClick={() => { }}
|
||||
user={defaultUser.data}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{users.data?.filter(user => user.id !== defaultUser.data?.id).map(user => (
|
||||
<div key={user.id}>
|
||||
<AccountEntry
|
||||
onClick={() => setDefaultUser(user)}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="self-center">
|
||||
|
||||
<DialogTrigger>
|
||||
<Button color="ghost">
|
||||
<PlusIcon />
|
||||
Add Account
|
||||
</Button>
|
||||
|
||||
<Overlay>
|
||||
<AddAccountModal />
|
||||
</Overlay>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Link to="/app/accounts">
|
||||
<Button color="ghost" size="iconLarge">
|
||||
<Settings01Icon className="stroke-fg-primary" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountRow({
|
||||
selected = false,
|
||||
profile,
|
||||
onPress,
|
||||
export default AccountPopup;
|
||||
|
||||
function AccountEntry({
|
||||
onClick,
|
||||
user,
|
||||
loggedIn = false,
|
||||
}: {
|
||||
selected?: boolean;
|
||||
profile: MinecraftCredentials;
|
||||
onPress: () => void;
|
||||
onClick: () => void;
|
||||
user: MinecraftCredentials;
|
||||
loggedIn?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<AriaButton
|
||||
className={twMerge(
|
||||
'flex flex-row items-center justify-start p-1 gap-2 hover:bg-component-bg-hover pressed:bg-component-bg-pressed rounded-lg',
|
||||
selected && 'bg-brand/80 pointer-events-none',
|
||||
)}
|
||||
onPress={onPress}
|
||||
slot="close"
|
||||
<div
|
||||
className={twMerge('flex flex-row justify-between p-2 rounded-lg', !loggedIn && 'hover:bg-component-bg-hover active:bg-component-bg-pressed hover:text-fg-primary-hover')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<AccountAvatar className="aspect-square h-8 rounded-md" uuid={profile.id} />
|
||||
|
||||
<div className="text-left flex flex-col">
|
||||
<p className="flex items-center gap-1 text-fg-primary font-medium">{profile.username}</p>
|
||||
<div className="flex flex-1 flex-row justify-start gap-x-3">
|
||||
<AccountAvatar className="h-8 w-8 rounded-md" uuid={user.id} />
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-start justify-between">
|
||||
<p className="h-[18px] font-semibold">{user.username}</p>
|
||||
{loggedIn && <p className="text-xs">Logged in</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AriaButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const popupVariants = tv({
|
|||
],
|
||||
dialog: [
|
||||
'flex flex-col items-center p-1 gap-2 relative',
|
||||
'border border-component-border-hover bg-page-elevated shadow-md rounded-lg',
|
||||
'border border-component-border-hover bg-page-elevated shadow-md rounded-xl',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
20
apps/oneclient/frontend/src/hooks/usePopState.ts
Normal file
20
apps/oneclient/frontend/src/hooks/usePopState.ts
Normal file
|
|
@ -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]);
|
||||
}
|
||||
25
apps/oneclient/frontend/src/hooks/useSettings.ts
Normal file
25
apps/oneclient/frontend/src/hooks/useSettings.ts
Normal file
|
|
@ -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(() => <TKey extends keyof Settings>(key: TKey) => settings.data[key], [settings]);
|
||||
|
||||
const setSetting = useMemo(() => <TKey extends keyof Settings>(key: TKey, value: Settings[TKey]) => {
|
||||
bindings.core.writeSettings({
|
||||
...settings.data,
|
||||
[key]: value,
|
||||
});
|
||||
settings.refetch();
|
||||
}, [settings]);
|
||||
|
||||
type Setter<TKey extends keyof Settings> = (value: Settings[TKey]) => void;
|
||||
|
||||
const createSetting = useMemo(() => <TKey extends keyof Settings>(key: TKey): [Settings[TKey], Setter<TKey>] => {
|
||||
return [setting(key), value => setSetting(key, value)];
|
||||
}, [setting]);
|
||||
|
||||
return { setting, setSetting, createSetting };
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Filters, PackageCategories, PackageType, Provider, Sort } from '@/bindings.gen';
|
||||
import { LoaderSuspense } from '@/components';
|
||||
import { LoaderSuspense, SheetPage } from '@/components';
|
||||
import { PackageGrid } from '@/components/PackageItem';
|
||||
import { bindings } from '@/main';
|
||||
import { browserCategories, categoryNameFromId } from '@/utils/browser';
|
||||
|
|
@ -33,15 +33,16 @@ const PROVIDERS: Array<Provider> = ['Modrinth', 'CurseForge', 'SkyClient'];
|
|||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
// <SheetPage.Content>
|
||||
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
|
||||
<div className="h-full w-full flex flex-1 flex-col items-center gap-y-2">
|
||||
<div className="grid grid-cols-[180px_auto_160px] w-full gap-x-6">
|
||||
<div />
|
||||
<div className="w-full grid grid-cols-[180px_auto] gap-x-6">
|
||||
<h2 className="flex-1 text-xxl font-semibold">Browser</h2>
|
||||
<BrowserToolbar />
|
||||
<div />
|
||||
{/* <div /> */}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[180px_auto_160px] w-full gap-x-6 pb-8">
|
||||
<div className="grid grid-cols-[180px_auto] w-full gap-x-6">
|
||||
<BrowserCategories />
|
||||
|
||||
<div className="h-full flex flex-col gap-y-4">
|
||||
|
|
@ -54,6 +55,7 @@ function RouteComponent() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
// </SheetPage.Content>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ function BrowserCategories() {
|
|||
|
||||
return (
|
||||
<div className="top-0 h-fit min-w-50">
|
||||
<div className="flex flex-col gap-y-6 ml-16">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<h5 className="my-1 text-sm uppercase opacity-60 font-light">Categories</h5>
|
||||
{browserCategories[categoryNameFromId[packageType]].map(category => (
|
||||
|
|
@ -111,7 +113,7 @@ function BrowserToolbar() {
|
|||
const [searchBar, setSearchBar] = useState(query);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-between bg-page">
|
||||
<div className="w-full flex flex-row justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
<Dropdown
|
||||
onSelectionChange={id => navigate({ search: { ...search, provider: id as Provider } })}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen';
|
||||
import type { ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl } from '@/bindings.gen';
|
||||
import { LoaderSuspense } from '@/components';
|
||||
import { ExternalLink } from '@/components/ExternalLink';
|
||||
import { Markdown } from '@/components/Markdown';
|
||||
import { bindings } from '@/main';
|
||||
import { abbreviateNumber, formatAsRelative, upperFirst, useCommand, useCommandMut, useCommandSuspense } from '@onelauncher/common';
|
||||
import { Button, Tooltip } from '@onelauncher/common/components';
|
||||
import { Button, Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { CalendarIcon, ClockRewindIcon, Download01Icon, File02Icon } from '@untitled-theme/icons-react';
|
||||
import { CalendarIcon, CheckIcon, ClockRewindIcon, Download01Icon, File02Icon, Loading02Icon, XIcon } from '@untitled-theme/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Cell, Column, Row, Tab, Table, TableBody, TableHeader, TabList, TabPanel, Tabs } from 'react-aria-components';
|
||||
|
||||
|
|
@ -349,6 +349,7 @@ function Versions() {
|
|||
function VersionRow({ version }: { version: ManagedVersion }) {
|
||||
const { cluster } = Route.useRouteContext();
|
||||
const { provider } = Route.useSearch();
|
||||
const download = useCommandMut(() => bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, true));
|
||||
return (
|
||||
<Row className="my-2 bg-page-elevated px-4 [&>td]:py-4 w-full">
|
||||
<Cell className="p-4 my-2 bg-component-bg rounded-l-xl">
|
||||
|
|
@ -365,17 +366,19 @@ function VersionRow({ version }: { version: ManagedVersion }) {
|
|||
<Cell className="p-4 pl-0 bg-component-bg">{version.downloads}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg rounded-r-xl">
|
||||
<Button
|
||||
children={<Download01Icon />}
|
||||
// children={download.isSuccess ? <CheckIcon /> : }
|
||||
color="secondary"
|
||||
onClick={() => downloadPackage(cluster, provider, version)}
|
||||
isDisabled={!download.isIdle}
|
||||
onClick={() => download.mutate()}
|
||||
size="icon"
|
||||
/>
|
||||
>
|
||||
<Show children={<Download01Icon />} when={download.isIdle} />
|
||||
<Show children={<CheckIcon />} when={download.isSuccess} />
|
||||
<Show children={<Loading02Icon className="animate-spin animate-duration-2000" />} when={download.isPending} />
|
||||
<Show children={<XIcon />} when={download.error} />
|
||||
</Button>
|
||||
|
||||
</Cell>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Provider } from '@/bindings.gen';
|
||||
import { SheetPage } from '@/components';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
|
||||
export interface BrowserRouteSearchParams {
|
||||
|
|
@ -16,6 +17,8 @@ export const Route = createFileRoute('/app/cluster/browser')({
|
|||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<Outlet />
|
||||
<SheetPage.Content>
|
||||
<Outlet />
|
||||
</SheetPage.Content>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
224
apps/oneclient/frontend/src/routes/app/settings/appearance.tsx
Normal file
224
apps/oneclient/frontend/src/routes/app/settings/appearance.tsx
Normal file
|
|
@ -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 (
|
||||
<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) => {
|
||||
return (
|
||||
<ThemeCard key={`${theme}-${variant.name}`} setTheme={() => { }} themeSelector={`${theme}-${variant.name}`} />
|
||||
);
|
||||
});
|
||||
})}
|
||||
</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"
|
||||
>
|
||||
<SettingsSwitch setting={createSetting('native_window_frame')} />
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Disables all animations in the launcher."
|
||||
icon={<Speedometer04Icon />}
|
||||
title="Disable Animations"
|
||||
>
|
||||
{/* <SettingsSwitch setting={createSetting("disable_animations")}/> */}
|
||||
</SettingsRow>
|
||||
</div>
|
||||
{/* </ScrollableContainer> */}
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface ThemeCardProps {
|
||||
themeSelector: string;
|
||||
setTheme: (merged: string) => void;
|
||||
};
|
||||
|
||||
function ThemeCard({
|
||||
themeSelector,
|
||||
setTheme,
|
||||
}: ThemeCardProps) {
|
||||
return (
|
||||
<div className={`h-min theme-${themeSelector}`} onClick={() => setTheme(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
|
||||
className="stroke-border"
|
||||
height="77"
|
||||
rx="7.5"
|
||||
stroke-opacity="0.1"
|
||||
width="125"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
/>
|
||||
<path
|
||||
className="stroke-fg-primary"
|
||||
d="M8 16H116"
|
||||
stroke-linecap="round"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<path
|
||||
className="stroke-fg-secondary"
|
||||
d="M8 26H56"
|
||||
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>
|
||||
);
|
||||
}
|
||||
77
apps/oneclient/frontend/src/routes/app/settings/index.tsx
Normal file
77
apps/oneclient/frontend/src/routes/app/settings/index.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// import DiscordIcon from '@/assets/logos/discord.svg';
|
||||
import SettingsRow from '@/components/SettingsRow';
|
||||
import SettingsSwitch from '@/components/SettingSwitch';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
import { bindings } from '@/main';
|
||||
// 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 { 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 { createSetting } = useSettings();
|
||||
const [launcherDir, setLauncherDir] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLauncherDir(await join(await dataDir(), 'OneLauncher'));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const openLauncherDir = () => bindings.core.open(launcherDir);
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<div className="h-full">
|
||||
<h1>General</h1>
|
||||
|
||||
<SettingsRow
|
||||
description="Enable Discord Rich Presence."
|
||||
icon={<Link03Icon />}
|
||||
title="Discord RPC"
|
||||
>
|
||||
<SettingsSwitch setting={createSetting('discord_enabled')} />
|
||||
</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"
|
||||
>
|
||||
<Toggle
|
||||
checked={() => !(settings().disable_analytics ?? false)}
|
||||
onChecked={value => settings().disable_analytics = !value}
|
||||
/>
|
||||
</SettingsRow> */}
|
||||
|
||||
<SettingsRow.Header>Folders and Files</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
description={launcherDir}
|
||||
icon={<FolderIcon />}
|
||||
title="Launcher Folder"
|
||||
>
|
||||
<Button onClick={openLauncherDir} size="normal">
|
||||
<LinkExternal01Icon />
|
||||
{' '}
|
||||
Open
|
||||
</Button>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
191
apps/oneclient/frontend/src/routes/app/settings/minecraft.tsx
Normal file
191
apps/oneclient/frontend/src/routes/app/settings/minecraft.tsx
Normal file
|
|
@ -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 (
|
||||
<Sidebar.Page>
|
||||
{/* <ScrollableContainer> */}
|
||||
<div className="h-full">
|
||||
<h1>Minecraft Settings</h1>
|
||||
|
||||
<GameSettings />
|
||||
|
||||
{/* <LauncherSettings /> */}
|
||||
|
||||
<ProcessSettings />
|
||||
</div>
|
||||
{/* </ScrollableContainer> */}
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
export function GameSettings() {
|
||||
const { createSetting } = useSettings();
|
||||
|
||||
const [gameSettings, setGameSettings] = createSetting('global_game_settings');
|
||||
|
||||
// const getSetting = useMemo(()=><TKey extends keyof SettingProfileModel>(key: TKey)=>gameSettings[key], [gameSettings])
|
||||
|
||||
const setSetting = useMemo(() => <TKey extends keyof SettingProfileModel>(key: TKey, value: SettingProfileModel[TKey]) => {
|
||||
setGameSettings({
|
||||
...gameSettings,
|
||||
[key]: value,
|
||||
});
|
||||
}, [gameSettings, setGameSettings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
description="Force Minecraft to start in fullscreen mode."
|
||||
icon={<Maximize01Icon />}
|
||||
title="Force Fullscreen"
|
||||
>
|
||||
<Switch
|
||||
isSelected={gameSettings.force_fullscreen ?? false}
|
||||
onChange={val => setSetting('force_fullscreen', val)}
|
||||
/>
|
||||
</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
|
||||
className="text-center"
|
||||
onChange={(e) => {
|
||||
setSetting('res', { height: gameSettings.res?.height ?? 720, width: Number(e.currentTarget.value) });
|
||||
}}
|
||||
type="number"
|
||||
value={gameSettings.res?.width}
|
||||
/>
|
||||
<XIcon className="size-4 self-center" />
|
||||
<TextField
|
||||
className="text-center"
|
||||
onChange={(e) => {
|
||||
setSetting('res', { width: gameSettings.res?.height ?? 1280, height: Number(e.currentTarget.value) });
|
||||
}}
|
||||
type="number"
|
||||
value={gameSettings.res?.height}
|
||||
/>
|
||||
</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">
|
||||
<TextField
|
||||
className="text-center"
|
||||
onChange={(e) => {
|
||||
setSetting('mem_max', Number(e.currentTarget.value));
|
||||
}}
|
||||
type="number"
|
||||
value={gameSettings.mem_max ?? undefined}
|
||||
/>
|
||||
</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 children="false" color="primary" />
|
||||
// </SettingsRow>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
export function ProcessSettings() {
|
||||
const { createSetting } = useSettings();
|
||||
|
||||
const [gameSettings, setGameSettings] = createSetting('global_game_settings');
|
||||
|
||||
// const getSetting = useMemo(()=><TKey extends keyof SettingProfileModel>(key: TKey)=>gameSettings[key], [gameSettings])
|
||||
|
||||
const setSetting = useMemo(() => <TKey extends keyof SettingProfileModel>(key: TKey, value: SettingProfileModel[TKey]) => {
|
||||
setGameSettings({
|
||||
...gameSettings,
|
||||
[key]: value,
|
||||
});
|
||||
}, [gameSettings, setGameSettings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRow.Header>Process</SettingsRow.Header>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run before launching the game."
|
||||
icon={<FilePlus02Icon />}
|
||||
title="Pre-Launch Command"
|
||||
>
|
||||
<TextField
|
||||
onChange={e => setSetting('hook_pre', e.currentTarget.value)}
|
||||
placeholder="echo 'Game started'"
|
||||
value={gameSettings.hook_pre ?? undefined}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run when launching the game."
|
||||
icon={<ParagraphWrapIcon />}
|
||||
title="Wrapper Command"
|
||||
>
|
||||
<TextField
|
||||
onChange={e => setSetting('hook_wrapper', e.currentTarget.value)}
|
||||
placeholder="gamescope"
|
||||
value={gameSettings.hook_wrapper ?? undefined}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
description="Command to run after exiting the game."
|
||||
icon={<FileX02Icon />}
|
||||
title="Post-Exit Command"
|
||||
>
|
||||
<TextField
|
||||
onChange={e => setSetting('hook_post', e.currentTarget.value)}
|
||||
placeholder="echo 'Game exited'"
|
||||
value={gameSettings.hook_post ?? undefined}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
135
apps/oneclient/frontend/src/routes/app/settings/route.tsx
Normal file
135
apps/oneclient/frontend/src/routes/app/settings/route.tsx
Normal file
|
|
@ -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 (
|
||||
<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 key="general" />, 'General', '/'],
|
||||
[<Brush01Icon key="appearence" />, 'Appearance', '/appearance'],
|
||||
// [<Key01Icon />, 'APIs', '/apis'],
|
||||
// [<Globe01Icon />, 'Language', '/language'],
|
||||
],
|
||||
'Game Settings': [
|
||||
[<Sliders04Icon key="mcsettings" />, 'Minecraft settings', '/minecraft'],
|
||||
],
|
||||
'About': [
|
||||
[<RefreshCcw02Icon key="changelog" />, 'Changelog', '/changelog'],
|
||||
[<MessageTextSquare01Icon key="feedback" />, 'Feedback', '/feedback'],
|
||||
[<CodeSnippet02Icon key="dev" />, 'Developer Options', '/developer'],
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{/* <Info /> */}
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
base: string;
|
||||
links: Record<string, Array<[JSX.Element, string, string, URLSearchParams?] | undefined>>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="w-56 flex flex-col pr-2">
|
||||
{Object.keys(links).map(section => (
|
||||
<div className="flex flex-col gap-y-2" key={section}>
|
||||
<div>
|
||||
<p className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</p>
|
||||
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
|
||||
{links[section].map((entry) => {
|
||||
if (!entry)
|
||||
return '';
|
||||
|
||||
const [Component, label, link, params] = entry;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={
|
||||
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link, params) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
|
||||
}
|
||||
key={link}
|
||||
onClick={() => goto(link, params)}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-x-3 *:w-5">
|
||||
{Component}
|
||||
{label}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarPageProps {
|
||||
className?: string;
|
||||
children: JSX.Element | Array<JSX.Element>;
|
||||
}
|
||||
|
||||
Sidebar.Page = function (props: SidebarPageProps) {
|
||||
return (
|
||||
<div {...props} className={`flex flex-col flex-1 gap-y-1 overflow-y-auto h-full ${props.className || ''}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
|
@ -462,7 +462,7 @@ impl From<CFPackage> for ManagedPackage {
|
|||
slug: value.slug,
|
||||
name: value.name,
|
||||
short_desc: value.summary,
|
||||
body: ManagedPackageBody::Url(format!("/v1/mods/{}/description", value.id)),
|
||||
body: ManagedPackageBody::Url(format!("/mods/{}/description", value.id)),
|
||||
version_ids: latest_version_ids,
|
||||
mc_versions,
|
||||
loaders,
|
||||
|
|
|
|||
|
|
@ -130,4 +130,8 @@ impl ProviderExt for Provider {
|
|||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
delegate!(self, get_versions(slugs))
|
||||
}
|
||||
|
||||
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
|
||||
delegate!(self, get_body(body))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue