chore: created a common package for frontend code

This commit is contained in:
LynithDev 2025-06-06 13:41:04 +02:00
parent 7d4acc2468
commit 9eadef559c
36 changed files with 336 additions and 273 deletions

View file

@ -4,8 +4,7 @@
},
"files.readonlyInclude": {
"**/node_modules/**": true,
"**/routeTree.gen.ts": true,
"**/bindings.gen.ts": true
"**/*.gen.ts": true
},
"files.watcherExclude": {
"**/_build": true,
@ -21,7 +20,7 @@
"**/.rustup": true,
"**/.cargo": true,
"**/.crates": true,
"**/routeTree.gen.ts": true
"**/*.gen.ts": true
},
"rust-analyzer.procMacro.enable": true,
"rust-analyzer.procMacro.attributes.enable": true,

View file

@ -3,3 +3,5 @@ node_modules
dist
dist-ssr
*.local
*.gen.ts

View file

@ -103,11 +103,11 @@ export interface SettingProfileModel { name: string; java_id: bigint | null; res
export interface SettingsOsExtra { enable_gamemode: boolean | null }
const ARGS_MAP = { 'core': '{"get_cluster":["id"],"remove_cluster":["id"],"get_profile_or_default":["name"],"get_clusters":[],"remove_user":["uuid"],"get_global_profile":[],"get_users":[],"get_default_user":["fallback"],"create_cluster":["options"],"open_msa_login":[],"get_user":["uuid"],"set_default_user":["uuid"]}', 'oneclient': '{"open_dev_tools":[],"return_error":[]}', '': '{"send_event":["event"]}' };
const ARGS_MAP = { 'core': '{"openMsaLogin":[],"getClusters":[],"getClusterById":["id"],"getUsers":[],"removeUser":["uuid"],"removeCluster":["id"],"getGlobalProfile":[],"getUser":["uuid"],"setDefaultUser":["uuid"],"createCluster":["options"],"getDefaultUser":["fallback"],"getProfileOrDefault":["name"]}', 'oneclient': '{"return_error":[],"open_dev_tools":[]}', '': '{"send_event":["event"]}' };
export interface Router {
'core': { get_clusters: () => Promise<Array<Model>>; get_cluster: (id: bigint) => Promise<Model | null>; remove_cluster: (id: bigint) => Promise<null>; create_cluster: (options: CreateCluster) => Promise<Model>; get_profile_or_default: (name: string | null) => Promise<SettingProfileModel>; get_global_profile: () => Promise<SettingProfileModel>; get_users: () => Promise<Array<MinecraftCredentials>>; get_user: (uuid: string) => Promise<MinecraftCredentials | null>; remove_user: (uuid: string) => Promise<null>; get_default_user: (fallback: boolean | null) => Promise<MinecraftCredentials | null>; set_default_user: (uuid: string | null) => Promise<null>; open_msa_login: () => Promise<MinecraftCredentials | null> };
'oneclient': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> };
'': { send_event: (event: LauncherEvent) => Promise<void> };
'core': { getClusters: () => Promise<Array<Model>>; getClusterById: (id: bigint) => Promise<Model | null>; removeCluster: (id: bigint) => Promise<null>; createCluster: (options: CreateCluster) => Promise<Model>; getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>; getGlobalProfile: () => Promise<SettingProfileModel>; getUsers: () => Promise<Array<MinecraftCredentials>>; getUser: (uuid: string) => Promise<MinecraftCredentials | null>; removeUser: (uuid: string) => Promise<null>; getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>; setDefaultUser: (uuid: string | null) => Promise<null>; openMsaLogin: () => Promise<MinecraftCredentials | null> };
'oneclient': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> };
}
export type { InferCommandOutput };

View file

@ -1,5 +1,5 @@
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Button } from "@onelauncher/common/components";
export const Route = createFileRoute('/')({
component: App,

View file

@ -3,3 +3,5 @@ node_modules
dist
dist-ssr
*.local
*.gen.ts

View file

@ -103,11 +103,11 @@ export interface SettingProfileModel { name: string; java_id: bigint | null; res
export interface SettingsOsExtra { enable_gamemode: boolean | null }
const ARGS_MAP = { '': '{"send_event":["event"]}', 'onelauncher': '{"return_error":[],"open_dev_tools":[]}', 'core': '{"get_user":["uuid"],"get_default_user":["fallback"],"set_default_user":["uuid"],"get_global_profile":[],"get_clusters":[],"remove_cluster":["id"],"get_profile_or_default":["name"],"get_users":[],"remove_user":["uuid"],"open_msa_login":[],"create_cluster":["options"],"get_cluster":["id"]}' };
const ARGS_MAP = { 'core': '{"getGlobalProfile":[],"getUsers":[],"removeUser":["uuid"],"getDefaultUser":["fallback"],"getUser":["uuid"],"getClusterById":["id"],"setDefaultUser":["uuid"],"openMsaLogin":[],"removeCluster":["id"],"getProfileOrDefault":["name"],"getClusters":[],"createCluster":["options"]}', 'onelauncher': '{"open_dev_tools":[],"return_error":[]}', '': '{"send_event":["event"]}' };
export interface Router {
'onelauncher': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> };
'core': { get_clusters: () => Promise<Array<Model>>; get_cluster: (id: bigint) => Promise<Model | null>; remove_cluster: (id: bigint) => Promise<null>; create_cluster: (options: CreateCluster) => Promise<Model>; get_profile_or_default: (name: string | null) => Promise<SettingProfileModel>; get_global_profile: () => Promise<SettingProfileModel>; get_users: () => Promise<Array<MinecraftCredentials>>; get_user: (uuid: string) => Promise<MinecraftCredentials | null>; remove_user: (uuid: string) => Promise<null>; get_default_user: (fallback: boolean | null) => Promise<MinecraftCredentials | null>; set_default_user: (uuid: string | null) => Promise<null>; open_msa_login: () => Promise<MinecraftCredentials | null> };
'': { send_event: (event: LauncherEvent) => Promise<void> };
'core': { getClusters: () => Promise<Array<Model>>; getClusterById: (id: bigint) => Promise<Model | null>; removeCluster: (id: bigint) => Promise<null>; createCluster: (options: CreateCluster) => Promise<Model>; getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>; getGlobalProfile: () => Promise<SettingProfileModel>; getUsers: () => Promise<Array<MinecraftCredentials>>; getUser: (uuid: string) => Promise<MinecraftCredentials | null>; removeUser: (uuid: string) => Promise<null>; getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>; setDefaultUser: (uuid: string | null) => Promise<null>; openMsaLogin: () => Promise<MinecraftCredentials | null> };
}
export type { InferCommandOutput };

View file

@ -1,15 +1,15 @@
import type { ToOptions } from '@tanstack/react-router';
import {useCommand} from '@onelauncher/common';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Menu } from '@onelauncher/common/components';
import { Link } from '@tanstack/react-router';
import { Bell01Icon, Cloud01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-react';
import { MenuTrigger } from 'react-aria-components';
import { Button, Menu } from '@onelauncher/common/components';
import PlayerHead from './content/PlayerHead';
import AccountPopup from './overlay/popups/AccountPopup';
function Navbar() {
const defaultUser = useCommand('getDefaultUser', () => bindings.core.get_default_user(false));
const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
return (
<div className="h-15 min-h-15 flex flex-row items-center *:flex-1">

View file

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

@ -1,4 +1,5 @@
import type { ImgHTMLAttributes, JSX } from 'react';
import { Show } from '@onelauncher/common/components';
import CurseforgeIcon from '../../assets/logos/curseforge.svg';
import AtLauncherIcon from '../../assets/logos/launchers/atlauncher.svg';
import FTBIcon from '../../assets/logos/launchers/ftb.svg';
@ -9,7 +10,6 @@ import TechnicImage from '../../assets/logos/launchers/technic.png';
import TLauncherImage from '../../assets/logos/launchers/tlauncher.png';
import UnknownImage from '../../assets/logos/minecraft.png';
import ModrinthIcon from '../../assets/logos/modrinth.svg';
import { Show } from '../base/Show';
type ElementType = (props: any) => JSX.Element;
@ -45,8 +45,8 @@ type LauncherIconProps = ImgHTMLAttributes<HTMLImageElement> & {
};
function LauncherIcon({
launcher,
...props
launcher,
...props
}: LauncherIconProps) {
return (
<Show

View file

@ -1,13 +1,13 @@
import type { Model, GameLoader } from '@/bindings.gen';
import type { GameLoader, Model } from '@/bindings.gen';
import type { ImgHTMLAttributes, RefAttributes } from 'react';
import FabricImage from '@/assets/logos/loaders/fabric.png';
import ForgeImage from '@/assets/logos/loaders/forge.png';
import NeoForgeImage from '@/assets/logos/loaders/neoforge.png';
import QuiltImage from '@/assets/logos/loaders/quilt.png';
import VanillaImage from '@/assets/logos/minecraft.png';
import type { RefAttributes } from 'react';
export function getLoaderLogoSrc(loader: Model | GameLoader): string {
const loaderName = (typeof loader === 'string' ? loader : loader.mc_loader)?.toLowerCase() as GameLoader;
const loaderName = (typeof loader === 'string' ? loader : loader.mc_loader).toLowerCase() as GameLoader;
const mapping: Record<GameLoader, string> = {
vanilla: VanillaImage,
@ -21,17 +21,21 @@ export function getLoaderLogoSrc(loader: Model | GameLoader): string {
return mapping[loaderName];
}
type LoaderIconProp = RefAttributes<HTMLImageElement> & {
type LoaderIconProp = ImgHTMLAttributes<HTMLImageElement> & RefAttributes<HTMLImageElement> & {
loader: GameLoader | undefined;
className?: string;
};
function LoaderIcon(props: LoaderIconProp) {
function LoaderIcon({
loader,
src: _src,
alt: _alt,
...rest
}: LoaderIconProp) {
return (
<img
{...props}
alt={`${props.loader}'s logo`}
src={props.loader ? getLoaderLogoSrc(props.loader) : VanillaImage}
{...rest}
alt={`${loader}'s logo`}
src={loader ? getLoaderLogoSrc(loader) : VanillaImage}
/>
);
}

View file

@ -1,19 +1,19 @@
import type { ClassNameString } from '@/types/global';
import {
Dialog as AriaDialog,
DialogTrigger,
Modal as ModalOverlay
} from 'react-aria-components';
import type { ClassNameString } from '@onelauncher/common';
import type { DialogProps } from 'react-aria-components';
import {
Dialog as AriaDialog,
DialogTrigger,
Modal as ModalOverlay,
} from 'react-aria-components';
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="" {...props} />
</ModalOverlay>
);
return (
<ModalOverlay className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" isDismissable>
<AriaDialog className="" {...props} />
</ModalOverlay>
);
}
Modal.Trigger = DialogTrigger;

View file

@ -1,17 +1,18 @@
import type { MinecraftCredentials } from '@/bindings.gen';
import PlayerHead from '@/components/content/PlayerHead';
import {useCommand} from '@onelauncher/common';
import { Button, Menu } from '@onelauncher/common/components';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Menu } from '@onelauncher/common/components';
import { PlusIcon, Settings01Icon } from '@untitled-theme/icons-react';
import { Separator } from 'react-aria-components';
import { twMerge } from 'tailwind-merge';
function AccountPopup() {
const users = useCommand('getUsers', bindings.commands.getUsers);
const defaultUser = useCommand('getDefaultUser', () => bindings.commands.getDefaultUser(false));
const users = useCommand('getUsers', bindings.core.getUsers);
const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
const setDefaultUser = (user: bindings.MinecraftCredentials) => {
bindings.commands.setDefaultUser(user.id);
const setDefaultUser = (user: MinecraftCredentials) => {
bindings.core.setDefaultUser(user.id);
};
return (
@ -64,7 +65,7 @@ function AccountEntry({
loggedIn = false,
}: {
onClick: () => void;
user: bindings.MinecraftCredentials;
user: MinecraftCredentials;
loggedIn?: boolean;
}) {
return (

View file

@ -1,10 +1,10 @@
import type { Model } from '@/bindings.gen';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { useEffect, useState } from 'react';
import useCommand from './useCommand';
function useRecentCluster() {
const result = useCommand('getClusters', bindings.core.get_clusters);
const result = useCommand('getClusters', bindings.core.getClusters);
const [cluster, setCluster] = useState<Model | undefined>();
useEffect(() => {

View file

@ -1,5 +1,5 @@
import { Button, TextField } from '@onelauncher/common/components';
import Modal from '@/components/overlay/Modal';
import { Button, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/browser/')({

View file

@ -3,12 +3,13 @@ import type { Dispatch, SetStateAction } from 'react';
import LoaderIcon from '@/components/launcher/LoaderIcon';
import ScrollableContainer from '@/components/ScrollableContainer';
import SettingsRow from '@/components/SettingsRow';
import useCommand from '@/hooks/useCommand';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Show, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/')({
@ -21,7 +22,7 @@ function RouteComponent() {
const [newName, setNewName] = useState<string>('');
// dumbass fix ik
const cluster = useCommand('getClusterById', () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint));
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
return (
<Sidebar.Page>
@ -102,7 +103,11 @@ interface BannerProps {
refetch: () => void;
}
function Banner(props: BannerProps) {
function Banner({
cluster,
editMode,
setNewName,
}: BannerProps) {
// async function launchFilePicker() {
// const selected = await dialog.open({
// multiple: false,
@ -123,13 +128,13 @@ function Banner(props: BannerProps) {
if (name.length > 30 || name.length <= 0)
return;
props.setNewName(name);
setNewName(name);
}
return (
<div className="h-37 flex flex-row gap-x-2.5 rounded-xl bg-page-elevated p-2.5">
<div className="relative aspect-ratio-video h-full min-w-57 w-57 overflow-hidden border border-component-bg/10 rounded-lg">
<Show when={props.editMode}>
<Show when={editMode}>
<div
className="absolute z-1 h-full w-full flex items-center justify-center bg-black/50 opacity-50 hover:opacity-100"
// onClick={launchFilePicker}
@ -146,37 +151,37 @@ function Banner(props: BannerProps) {
<div>
<Show
fallback={
<h2 className="break-words text-wrap text-2xl">{props.cluster?.name}</h2>
<h2 className="break-words text-wrap text-2xl">{cluster?.name}</h2>
}
when={props.editMode}
when={editMode}
>
<TextField
className="text-xl font-bold"
onChange={e => updateName(e.target.value)}
placeholder={props.cluster?.name}
placeholder={cluster?.name}
/>
</Show>
</div>
<div className="flex flex-1 flex-row">
<div
className={`flex flex-1 flex-col items-start justify-between ${props.editMode}` ? 'text-fg-primary-disabled' : ''}
className={twMerge(`flex flex-1 flex-col items-start justify-between`, editMode && 'text-fg-primary-disabled')}
>
<span className="flex flex-row items-center gap-x-1">
<LoaderIcon
className="w-5"
loader={props.cluster?.mc_loader}
loader={cluster?.mc_loader}
/>
<span>{props.cluster?.mc_version}</span>
<span>{props.cluster?.mc_loader || 'unknown'}</span>
{props.cluster?.mc_loader_version && <span>{props.cluster.mc_loader_version}</span>}
<span>{cluster?.mc_version}</span>
<span>{cluster?.mc_loader || 'unknown'}</span>
{cluster?.mc_loader_version && <span>{cluster.mc_loader_version}</span>}
</span>
<span
className={`text-xs text-fg-secondary ${props.editMode}` ? 'text-fg-secondary-disabled' : ''}
className={twMerge(`text-xs text-fg-secondary`, editMode && 'text-fg-secondary-disabled')}
>
Played for
{' '}
<b>{props.cluster?.overall_played || 0}</b>
<b>{cluster?.overall_played || 0}</b>
.
</span>
</div>

View file

@ -1,52 +1,52 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'
import Sidebar from '../settings/route'
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-react'
import useCommand from '@/hooks/useCommand'
import { bindings } from '@/main'
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-react';
import Sidebar from '../settings/route';
type ClusterSearch = {
id: bigint
interface ClusterSearch {
id: bigint;
}
export const Route = createFileRoute('/app/cluster')({
component: RouteComponent,
validateSearch: (search: Record<string, unknown>): ClusterSearch => {
return {
id: BigInt(search.id as string),
}
},
})
component: RouteComponent,
validateSearch: (search: Record<string, unknown>): ClusterSearch => {
return {
id: BigInt(search.id as string),
};
},
});
function RouteComponent() {
const { id } = Route.useSearch()
const { id } = Route.useSearch();
const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint))
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
const isModded = cluster.data?.mc_loader != "vanilla"
const isModded = cluster.data?.mc_loader != 'vanilla';
return (
<div className="h-full flex flex-row overflow-hidden">
<div className="flex-shrink-0 w-72 flex flex-col pt-8 pr-7 pb-8">
<Sidebar
base="/app/cluster"
links={{
'Cluster Settings': [
// if someone has a better way to do this, please let me know
[<EyeIcon />, 'Overview', '/?id=' + id],
(isModded ? [<PackagePlusIcon />, 'Mods', '/mods?id=' + id] : undefined),
[<Image03Icon />, 'Screenshots', '/screenshots?id=' + id],
[<Globe04Icon />, 'Worlds', '/worlds?id=' + id],
[<File06Icon />, 'Logs', '/logs?id=' + id],
[<Settings04Icon />, 'Game Settings', '/settings?id=' + id],
],
}}
/>
{/* <Info /> */}
</div>
return (
<div className="h-full flex flex-row overflow-hidden">
<div className="flex-shrink-0 w-72 flex flex-col pt-8 pr-7 pb-8">
<Sidebar
base="/app/cluster"
links={{
'Cluster Settings': [
// if someone has a better way to do this, please let me know
[<EyeIcon />, 'Overview', `/?id=${id}`],
(isModded ? [<PackagePlusIcon />, 'Mods', `/mods?id=${id}`] : undefined),
[<Image03Icon />, 'Screenshots', `/screenshots?id=${id}`],
[<Globe04Icon />, 'Worlds', `/worlds?id=${id}`],
[<File06Icon />, 'Logs', `/logs?id=${id}`],
[<Settings04Icon />, 'Game Settings', `/settings?id=${id}`],
],
}}
/>
{/* <Info /> */}
</div>
<div className="flex-1 min-w-0 h-full">
<Outlet />
</div>
</div>
)
<div className="flex-1 min-w-0 h-full">
<Outlet />
</div>
</div>
);
}

View file

@ -1,46 +1,51 @@
import { createFileRoute } from '@tanstack/react-router'
import Sidebar from '../settings/route';
import ScrollableContainer from '@/components/ScrollableContainer';
import { Show } from '@/components/base/Show';
import { Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/screenshots')({
component: RouteComponent,
})
component: RouteComponent,
});
function RouteComponent() {
const arr = [1, 2, 3, 4, 5, 6]
return (
<Sidebar.Page>
<h1>Screenshots</h1>
<ScrollableContainer>
<div className="h-full">
<Show
fallback={<div className="text-border/400">No screenshots found. Press F2 in game to take a screenshot!</div>}
when={true}
>
<div className="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full transform-gpu gap-2">
{arr.map((data) => (
<ScreenshotEntry key={data} path={`screenshot-${data}.png`} cluster_path="cluster" />
))}
</div>
</Show>
</div>
</ScrollableContainer>
</Sidebar.Page>
)
const arr = [1, 2, 3, 4, 5, 6];
return (
<Sidebar.Page>
<h1>Screenshots</h1>
<ScrollableContainer>
<div className="h-full">
<Show
fallback={<div className="text-border/400">No screenshots found. Press F2 in game to take a screenshot!</div>}
when
>
<div className="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full transform-gpu gap-2">
{arr.map(data => (
<ScreenshotEntry cluster_path="cluster" key={data} path={`screenshot-${data}.png`} />
))}
</div>
</Show>
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}
function ScreenshotEntry(props: { path: string; cluster_path: string }) {
function onClick() {
//screen shot viewer overlay
}
function ScreenshotEntry({
path,
}: {
path: string;
cluster_path: string;
}) {
function onClick() {
// screen shot viewer overlay
}
return (
<div
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80"
onClick={onClick}
>
<img alt={props.path} className="aspect-video w-full rounded-lg" src="https://github.com/emirsassan.png" />
</div>
);
return (
<div
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80"
onClick={onClick}
>
<img alt={path} className="aspect-video w-full rounded-lg" src="https://github.com/emirsassan.png" />
</div>
);
}

View file

@ -1,35 +1,33 @@
import { createFileRoute } from '@tanstack/react-router'
import Sidebar from '../settings/route'
import ScrollableContainer from '@/components/ScrollableContainer'
import useCommand from '@/hooks/useCommand'
import { bindings } from '@/main'
import { GameSettings, ProcessSettings } from '../settings/minecraft'
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { createFileRoute } from '@tanstack/react-router';
import { GameSettings, ProcessSettings } from '../settings/minecraft';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/settings')({
component: RouteComponent,
})
component: RouteComponent,
});
function RouteComponent() {
const { id } = Route.useSearch()
const { id } = Route.useSearch();
const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint))
const result = useCommand("getProfileOrDefault", () => bindings.commands.getProfileOrDefault(cluster.data?.name as string), {
enabled: !!cluster.data?.name
})
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
const result = useCommand('getProfileOrDefault', () => bindings.core.getProfileOrDefault(cluster.data?.name as string), {
enabled: !!cluster.data?.name,
});
console.log(result.data)
return (
<Sidebar.Page>
<ScrollableContainer>
<div className="h-full">
<h1>Minecraft Settings</h1>
return (
<Sidebar.Page>
<ScrollableContainer>
<div className="h-full">
<h1>Minecraft Settings</h1>
<GameSettings />
<GameSettings />
<ProcessSettings />
</div>
</ScrollableContainer>
</Sidebar.Page>
)
<ProcessSettings />
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -1,20 +1,20 @@
import { createFileRoute } from '@tanstack/react-router'
import Sidebar from '../settings/route'
import ScrollableContainer from '@/components/ScrollableContainer'
import ScrollableContainer from '@/components/ScrollableContainer';
import { createFileRoute } from '@tanstack/react-router';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/worlds')({
component: RouteComponent,
})
component: RouteComponent,
});
function RouteComponent() {
return (
<Sidebar.Page>
<h1>Worlds</h1>
<ScrollableContainer>
<div className="h-full">
</div>
</ScrollableContainer>
</Sidebar.Page>
)
return (
<Sidebar.Page>
<h1>Worlds</h1>
<ScrollableContainer>
<div className="h-full">
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -6,7 +6,7 @@ import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, TextField } from '@onelauncher/common/components';
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
import { createFileRoute, Link } from '@tanstack/react-router';
import { Server01Icon } from '@untitled-theme/icons-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
@ -19,7 +19,7 @@ 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.core.get_clusters);
const result = useCommand('getClusters', bindings.core.getClusters);
return (
<div className="h-full flex flex-col gap-y-4 text-fg-primary">
@ -49,7 +49,7 @@ function Banner() {
*/
// const cluster = useRecentCluster();
// const launch = useLaunchCluster(() => cluster()?.uuid);
const navigate = useNavigate();
// const navigate = useNavigate();
return (
<div className="relative h-52 min-h-52 w-full overflow-hidden rounded-xl border border-component-border">
@ -110,8 +110,8 @@ function Banner() {
}
function ClusterCreate() {
const result = useCommand('createCluster', () => bindings.commands.createCluster({
icon_url: 'asd',
const result = useCommand('createCluster', () => bindings.core.createCluster({
icon: 'asd',
mc_loader: 'vanilla',
mc_version: '1.20.1',
name: 'Test Cluster',
@ -177,8 +177,11 @@ interface ClusterGroupProps {
isFetching?: boolean;
}
function ClusterGroup(props: ClusterGroupProps) {
if (props.isFetching)
function ClusterGroup({
isFetching,
clusters,
}: ClusterGroupProps) {
if (isFetching)
return (
<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" />
@ -191,7 +194,7 @@ function ClusterGroup(props: ClusterGroupProps) {
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 => (
{clusters?.map(data => (
<ClusterCard key={data.id} {...data} />
))}
</div>
@ -200,11 +203,16 @@ function ClusterGroup(props: ClusterGroupProps) {
);
}
function ClusterCard(props: Model) {
function ClusterCard({
id,
name,
mc_loader,
mc_version,
}: Model) {
return (
<Link
search={{
id: props.id,
id,
}} to="/app/cluster"
>
<div
@ -222,11 +230,11 @@ function ClusterCard(props: Model) {
</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-ellipsis whitespace-nowrap font-medium">{name}</p>
<p className="h-4 text-xs">
{props.mc_loader}
{mc_loader}
{' '}
{props.mc_version}
{mc_version}
{/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>

View file

@ -1,7 +1,7 @@
import PlayerHead from '@/components/content/PlayerHead';
import ScrollableContainer from '@/components/ScrollableContainer';
import useCommand from '@/hooks/useCommand';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Show, Tooltip } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { InfoCircleIcon, Trash01Icon, UserPlus02Icon } from '@untitled-theme/icons-react';
@ -18,8 +18,8 @@ interface Account {
}
function RouteComponent() {
const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.commands.getUsers);
const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.commands.getDefaultUser(false));
const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.core.getUsers);
const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
if (usersLoading || defaultUserLoading)
return (
@ -67,7 +67,7 @@ function AccountRow({ account, isCurrent }: AccountRowProps) {
const setDefaultUserCommand = useCommand(
'setDefaultUser',
() => bindings.commands.setDefaultUser(account.id),
() => bindings.core.setDefaultUser(account.id),
{
enabled: false,
subscribed: false,
@ -76,7 +76,7 @@ function AccountRow({ account, isCurrent }: AccountRowProps) {
const removeUserCommand = useCommand(
'removeUser',
() => bindings.commands.removeUser(account.id),
() => bindings.core.removeUser(account.id),
{
enabled: false,
subscribed: false,

View file

@ -77,9 +77,12 @@ interface ThemeCardProps {
setTheme: (merged: string) => void;
};
function ThemeCard(props: ThemeCardProps) {
function ThemeCard({
themeSelector,
setTheme,
}: ThemeCardProps) {
return (
<div className={`h-min theme-${props.themeSelector}`} onClick={() => props.setTheme(props.themeSelector)}>
<div className={`h-min theme-${themeSelector}`} onClick={() => setTheme(themeSelector)}>
<svg
fill="none"
height="78"

View file

@ -12,7 +12,7 @@ export const Route = createFileRoute('/app/settings/')({
});
function RouteComponent() {
const result = useCommand('getGlobalProfile', bindings.commands.getGlobalProfile);
const result = useCommand('getGlobalProfile', bindings.core.getGlobalProfile);
return (
<Sidebar.Page>

View file

@ -1,8 +1,8 @@
import ScrollableContainer from '@/components/ScrollableContainer';
import SettingsRow from '@/components/SettingsRow';
import { Switch, TextField, ToggleButton } from '@onelauncher/common/components';
import { Switch, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react';
import { Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react';
import Sidebar from './route';
export const Route = createFileRoute('/app/settings/minecraft')({

View file

@ -100,8 +100,7 @@ function Sidebar(props: SidebarProps) {
</div>
</div>
</div>
),
)}
))}
</div>
);
}

View file

@ -1,9 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboarding/complete')({
component: RouteComponent,
})
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/onboarding/complete"!</div>
return <div>Hello "/onboarding/complete"!</div>;
}

View file

@ -1,19 +1,19 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboarding/')({
component: RouteComponent,
})
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="grid grid-cols-2 h-full w-full items-center gap-x-16">
<h1 className="text-6xl">
Welcome to
{' '}
<span className="underline underline-8 underline-brand">OneLauncher</span>
</h1>
return (
<div className="grid grid-cols-2 h-full w-full items-center gap-x-16">
<h1 className="text-6xl">
Welcome to
{' '}
<span className="underline underline-8 underline-brand">OneLauncher</span>
</h1>
<h3>A powerful yet easy to use launcher for Minecraft.</h3>
</div>
)
<h3>A powerful yet easy to use launcher for Minecraft.</h3>
</div>
);
}

View file

@ -13,7 +13,7 @@ export const Route = createFileRoute('/onboarding/login')({
function RouteComponent() {
const [profile, setProfile] = useState<MinecraftCredentials>();
const [errorMessage, setErrorMessage] = useState<string>('');
const result = useCommand('beginMsFlow', bindings.commands.beginMsFlow, {
const result = useCommand('openMsaLogin', bindings.core.openMsaLogin, {
enabled: false,
subscribed: false,
});

View file

@ -120,21 +120,26 @@ export interface OnboardingStepProps {
children: JSX.Element;
}
export function OnboardingStep(props: OnboardingStepProps) {
export function OnboardingStep({
title,
paragraph,
illustration,
children,
}: OnboardingStepProps) {
return (
<div className="grid grid-cols-2 h-full w-full gap-x-16">
<div className="flex flex-col items-center justify-center">
{props.illustration}
{illustration}
</div>
<div className="flex flex-col justify-center gap-y-4">
<div className="w-full flex flex-col gap-y-2">
<h1 className="text-2xl">{props.title}</h1>
<p className="text-lg text-fg-secondary line-height-normal">{props.paragraph}</p>
<h1 className="text-2xl">{title}</h1>
<p className="text-lg text-fg-secondary line-height-normal">{paragraph}</p>
</div>
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
{props.children}
{children}
</div>
</div>
</div>

View file

@ -1,3 +1,9 @@
export interface ClassNameString {
className?: string;
import type { bindings } from '@/main';
export type BindingCommands = (keyof typeof bindings.core) | (keyof typeof bindings.onelauncher);
declare module '@onelauncher/common' {
interface Register {
commands: Array<BindingCommands>;
}
}

View file

@ -27,5 +27,5 @@
"skipLibCheck": true,
"noUncheckedSideEffectImports": true
},
"include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"]
"include": ["**/*.ts", "**/*.tsx", "vite.config.js"]
}

View file

@ -8,9 +8,6 @@ export default defineConfig(
react: true,
query: true,
ignores: ['*.rs', '**/migrations/**'],
rules: {
'ts/no-use-before-define': 'off',
},
toml: {
overrides: {
'toml/array-element-newline': ['error', 'always'],
@ -30,7 +27,9 @@ export default defineConfig(
rules: {
'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports',
'import/order': 'off', // Replaced by perfectionist/sort-named-imports',
'style/jsx-max-props-per-line': ['error', { maximum: 3}]
'style/jsx-max-props-per-line': ['error', { maximum: 3}],
'ts/no-use-before-define': 'off',
'style/function-paren-newline': ['error', 'consistent']
},
},
);

View file

@ -9,21 +9,43 @@ use onelauncher_entity::prelude::model::*;
pub trait TauriLauncherApi {
// Clusters
#[taurpc(alias = "getClusters")]
async fn get_clusters() -> LauncherResult<Vec<Cluster>>;
async fn get_cluster(id: ClusterId) -> LauncherResult<Option<Cluster>>;
#[taurpc(alias = "getClusterById")]
async fn get_cluster_by_id(id: ClusterId) -> LauncherResult<Option<Cluster>>;
#[taurpc(alias = "removeCluster")]
async fn remove_cluster(id: ClusterId) -> LauncherResult<()>;
#[taurpc(alias = "createCluster")]
async fn create_cluster(options: CreateCluster) -> LauncherResult<Cluster>;
// Setting Profiles
#[taurpc(alias = "getProfileOrDefault")]
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
#[taurpc(alias = "getGlobalProfile")]
async fn get_global_profile() -> SettingsProfile;
// Users
#[taurpc(alias = "getUsers")]
async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>;
#[taurpc(alias = "getUser")]
async fn get_user(uuid: uuid::Uuid) -> LauncherResult<Option<MinecraftCredentials>>;
#[taurpc(alias = "removeUser")]
async fn remove_user(uuid: uuid::Uuid) -> LauncherResult<()>;
#[taurpc(alias = "getDefaultUser")]
async fn get_default_user(fallback: Option<bool>) -> LauncherResult<Option<MinecraftCredentials>>;
#[taurpc(alias = "setDefaultUser")]
async fn set_default_user(uuid: Option<uuid::Uuid>) -> LauncherResult<()>;
#[taurpc(alias = "openMsaLogin")]
async fn open_msa_login<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>>;
}
@ -48,7 +70,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::cluster::dao::get_all_clusters().await
}
async fn get_cluster(self, id: ClusterId) -> LauncherResult<Option<Cluster>> {
async fn get_cluster_by_id(self, id: ClusterId) -> LauncherResult<Option<Cluster>> {
api::cluster::dao::get_cluster_by_id(id).await
}

View file

@ -116,8 +116,7 @@ export function AnimatedOutlet({
const handleAnimationComplete = (id: number) => {
setSnapshots(prevSnapshots =>
prevSnapshots.filter(snapshot => snapshot.id !== id),
);
prevSnapshots.filter(snapshot => snapshot.id !== id));
};
return (

View file

@ -1,11 +1,18 @@
import type { UndefinedInitialDataOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
type CacheKey = string;
// this gets overwritten by the consumer project
export interface Register {
// commands
}
export function useCommand<T>(cacheKey: CacheKey | (string & {}), command: () => Promise<T>, options?: Omit<UndefinedInitialDataOptions<T>, 'queryKey' | 'queryFn'>): UseQueryResult<T, Error> {
type RegisterConfig = Register extends {
commands: infer Keys;
} ? Keys : never;
export function useCommand<T>(cacheKey: RegisterConfig[number] | (string & {}) | false, command: () => Promise<T>, options?: Omit<UndefinedInitialDataOptions<T>, 'queryKey' | 'queryFn'>): UseQueryResult<T, Error> {
return useQuery({
queryKey: [cacheKey],
queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: async () => {
try {
const result = await command();
@ -24,5 +31,3 @@ export function useCommand<T>(cacheKey: CacheKey | (string & {}), command: () =>
...options,
});
}
// export useCommand;

View file

@ -1,2 +1,3 @@
export * from './hooks';
export type * from './types/global';
export * from './utils';