diff --git a/.vscode/settings.json b/.vscode/settings.json index b7be1aee..e539bbb3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, diff --git a/Cargo.lock b/Cargo.lock index 6727a4ff..34bbd097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3388,6 +3388,7 @@ version = "0.2.0-alpha.2" dependencies = [ "anyhow", "async-compression", + "async-stream", "async-trait", "async_zip", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index c2860f37..033bcae0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ sea-orm-migration = { version = "1.1", features = [ # utils anyhow = { version = "1" } async-trait = { version = "=0.1.88" } +async-stream = { version = "=0.3.6" } cc = { version = "1" } discord-rich-presence = { version = "=0.2.5" } futures = { version = "0.3" } diff --git a/apps/oneclient/frontend/.gitignore b/apps/oneclient/frontend/.gitignore index d451ff16..d195a4a0 100644 --- a/apps/oneclient/frontend/.gitignore +++ b/apps/oneclient/frontend/.gitignore @@ -3,3 +3,5 @@ node_modules dist dist-ssr *.local + +*.gen.ts diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 6b4f2cd0..ad95f510 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -42,8 +42,6 @@ export type JavaError = { ParseVersion: [string] } | 'Execute' | 'MissingJava'; export type LauncherError = { type: 'DirError'; data: DirectoryError } | { type: 'IOError'; data: IOError } | { type: 'IngressError'; data: IngressError } | { type: 'JavaError'; data: JavaError } | { type: 'CryptoError'; data: CryptoError } | { type: 'DiscordError'; data: DiscordError } | { type: 'MetadataError'; data: MetadataError } | { type: 'ClusterError'; data: ClusterError } | { type: 'MinecraftAuthError'; data: MinecraftAuthError } | { type: 'ProcessError'; data: ProcessError } | { type: 'PackageError'; data: PackageError } | { type: 'DaoError'; data: DaoError } | { type: 'SerdeError' } | { type: 'AnyhowError' } | { type: 'DbError' } | { type: 'ReqwestError' } | { type: 'InterpulseError' } | { type: 'RegexError' } | { type: 'SemaphoreError' } | { type: 'UrlError' } | { type: 'TauriError' }; -export type LauncherEvent = { Ingress: IngressPayload } | { Message: MessagePayload } | { Process: ProcessPayload }; - export type MessageLevel = 'Info' | 'Warn' | 'Error'; export interface MessagePayload { level: MessageLevel; message: string } @@ -95,7 +93,7 @@ export interface Process { pid: number; started_at: string; cluster_id: bigint; export type ProcessError = { HookUnsuccessful: number } | 'NoPid'; -export type ProcessPayload = { Starting: { command: string } } | { Started: { process: Process } } | { Stopped: { pid: number; exit_code: number } } | { Output: { pid: number; output: string } }; +export type ProcessPayload = { type: 'Starting'; command: string } | { type: 'Started'; process: Process } | { type: 'Stopped'; pid: number; exit_code: number } | { type: 'Output'; pid: number; output: string }; export interface Resolution { width: number; height: number } @@ -103,11 +101,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 = { oneclient: '{"open_dev_tools":[],"return_error":[]}', core: '{"getClusters":[],"createCluster":["options"],"getGlobalProfile":[],"getClusterById":["id"],"openMsaLogin":[],"getUsers":[],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"getUser":["uuid"],"getProfileOrDefault":["name"],"removeUser":["uuid"],"removeCluster":["id"],"launchCluster":["id","uuid"]}', events: '{"message":["event"],"process":["event"],"ingress":["event"]}' }; export interface Router { - 'core': { get_clusters: () => Promise>; get_cluster: (id: bigint) => Promise; remove_cluster: (id: bigint) => Promise; create_cluster: (options: CreateCluster) => Promise; get_profile_or_default: (name: string | null) => Promise; get_global_profile: () => Promise; get_users: () => Promise>; get_user: (uuid: string) => Promise; remove_user: (uuid: string) => Promise; get_default_user: (fallback: boolean | null) => Promise; set_default_user: (uuid: string | null) => Promise; open_msa_login: () => Promise }; - 'oneclient': { return_error: () => Promise; open_dev_tools: () => Promise }; - '': { send_event: (event: LauncherEvent) => Promise }; + events: { ingress: (event: IngressPayload) => Promise; message: (event: MessagePayload) => Promise; process: (event: ProcessPayload) => Promise }; + core: { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; launchCluster: (id: bigint, uuid: string | null) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise }; + oneclient: { return_error: () => Promise; open_dev_tools: () => Promise }; } export type { InferCommandOutput }; diff --git a/apps/oneclient/frontend/src/routes/index.tsx b/apps/oneclient/frontend/src/routes/index.tsx index 90519972..7f4a4afa 100644 --- a/apps/oneclient/frontend/src/routes/index.tsx +++ b/apps/oneclient/frontend/src/routes/index.tsx @@ -1,15 +1,36 @@ +import { bindings } from '@/main'; +import { Button } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { Button } from "@onelauncher/common/components"; export const Route = createFileRoute('/')({ component: App, }); function App() { + const onClick = async () => { + // const result = await bindings.core.createCluster({ + // name: 'Test 1.8.9 Forge', + // mc_loader: 'forge', + // mc_version: '1.8.9', + // icon: null, + // mc_loader_version: '11.15.1.2318', + // }); + + await bindings.core.launchCluster(2 as unknown as bigint, null); + + const unlisten = await bindings.events.process.on((e) => { + if (e.type === 'Output') { console.log(e.output); } + else if (e.type === 'Stopped') { + console.log(e.exit_code); + unlisten(); + } + }); + }; + return (

test

- +
); } diff --git a/apps/onelauncher/frontend/.gitignore b/apps/onelauncher/frontend/.gitignore index d451ff16..d195a4a0 100644 --- a/apps/onelauncher/frontend/.gitignore +++ b/apps/onelauncher/frontend/.gitignore @@ -3,3 +3,5 @@ node_modules dist dist-ssr *.local + +*.gen.ts diff --git a/apps/onelauncher/frontend/src/bindings.gen.ts b/apps/onelauncher/frontend/src/bindings.gen.ts index 6a4a1279..0803d759 100644 --- a/apps/onelauncher/frontend/src/bindings.gen.ts +++ b/apps/onelauncher/frontend/src/bindings.gen.ts @@ -117,12 +117,16 @@ set_default_user: (uuid: string | null) => Promise, open_msa_login: () => Promise }, '': { send_event: (event: LauncherEvent) => Promise } }; +<<<<<<< HEAD <<<<<<< Updated upstream 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"]}' }; +>>>>>>> 17734ca1b9033a35ee6a581cc55e25fa7fc932b3 export interface Router { 'onelauncher': { return_error: () => Promise; open_dev_tools: () => Promise }; - 'core': { get_clusters: () => Promise>; get_cluster: (id: bigint) => Promise; remove_cluster: (id: bigint) => Promise; create_cluster: (options: CreateCluster) => Promise; get_profile_or_default: (name: string | null) => Promise; get_global_profile: () => Promise; get_users: () => Promise>; get_user: (uuid: string) => Promise; remove_user: (uuid: string) => Promise; get_default_user: (fallback: boolean | null) => Promise; set_default_user: (uuid: string | null) => Promise; open_msa_login: () => Promise }; '': { send_event: (event: LauncherEvent) => Promise }; + 'core': { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise }; } ======= >>>>>>> Stashed changes diff --git a/apps/onelauncher/frontend/src/components/Navbar.tsx b/apps/onelauncher/frontend/src/components/Navbar.tsx index d37dc4ca..f84e01f6 100644 --- a/apps/onelauncher/frontend/src/components/Navbar.tsx +++ b/apps/onelauncher/frontend/src/components/Navbar.tsx @@ -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 (
diff --git a/apps/onelauncher/frontend/src/components/SettingsRow.tsx b/apps/onelauncher/frontend/src/components/SettingsRow.tsx index b5cd826a..ce336cae 100644 --- a/apps/onelauncher/frontend/src/components/SettingsRow.tsx +++ b/apps/onelauncher/frontend/src/components/SettingsRow.tsx @@ -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 ( -
-
- {props.icon} -
+ > +
+ {icon} +
-
-

{props.title}

-

{props.description}

-
+
+

{title}

+

{description}

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

{children}

; + const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`; + return

{children}

; }; export default SettingsRow; diff --git a/apps/onelauncher/frontend/src/components/content/LauncherIcon.tsx b/apps/onelauncher/frontend/src/components/content/LauncherIcon.tsx index 1acae0c1..5536ab92 100644 --- a/apps/onelauncher/frontend/src/components/content/LauncherIcon.tsx +++ b/apps/onelauncher/frontend/src/components/content/LauncherIcon.tsx @@ -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 & { }; function LauncherIcon({ - launcher, - ...props + launcher, + ...props }: LauncherIconProps) { return ( = { vanilla: VanillaImage, @@ -21,17 +21,21 @@ export function getLoaderLogoSrc(loader: Model | GameLoader): string { return mapping[loaderName]; } -type LoaderIconProp = RefAttributes & { +type LoaderIconProp = ImgHTMLAttributes & RefAttributes & { loader: GameLoader | undefined; - className?: string; }; -function LoaderIcon(props: LoaderIconProp) { +function LoaderIcon({ + loader, + src: _src, + alt: _alt, + ...rest +}: LoaderIconProp) { return ( {`${props.loader}'s ); } diff --git a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx index 728cd4da..cc32aa79 100644 --- a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx @@ -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 ( - - - - ); + return ( + + + + ); } Modal.Trigger = DialogTrigger; diff --git a/apps/onelauncher/frontend/src/components/overlay/popups/AccountPopup.tsx b/apps/onelauncher/frontend/src/components/overlay/popups/AccountPopup.tsx index 954ed90d..8caaafc3 100644 --- a/apps/onelauncher/frontend/src/components/overlay/popups/AccountPopup.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/popups/AccountPopup.tsx @@ -8,11 +8,11 @@ import { Separator } from 'react-aria-components'; import { twMerge } from 'tailwind-merge'; function AccountPopup() { - const users = useCommand('getUsers', bindings.core.get_users); - const defaultUser = useCommand('getDefaultUser', () => bindings.core.get_default_user(false)); + const users = useCommand('getUsers', bindings.core.getUsers); + const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false)); const setDefaultUser = (user: MinecraftCredentials) => { - bindings.core.set_default_user(user.id); + bindings.core.setDefaultUser(user.id); }; return ( diff --git a/apps/onelauncher/frontend/src/hooks/useCluster.ts b/apps/onelauncher/frontend/src/hooks/useCluster.ts index 3c764a98..eb8e44e7 100644 --- a/apps/onelauncher/frontend/src/hooks/useCluster.ts +++ b/apps/onelauncher/frontend/src/hooks/useCluster.ts @@ -4,7 +4,7 @@ import { useCommand } from '@onelauncher/common'; import { useEffect, useState } from 'react'; function useRecentCluster() { - const result = useCommand('getClusters', bindings.core.get_clusters); + const result = useCommand('getClusters', bindings.core.getClusters); const [cluster, setCluster] = useState(); useEffect(() => { diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx index 096b860b..e058f6e9 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx @@ -9,6 +9,7 @@ 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(''); // dumbass fix ik - const cluster = useCommand('getClusterById', () => bindings.core.get_cluster(Number(id.toString()) as unknown as bigint)); + const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint)); return ( @@ -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, @@ -162,7 +167,7 @@ function Banner(props: BannerProps) {
{cluster.mc_loader_version}} Played for {' '} diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx index fd79613c..3596750c 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx @@ -20,9 +20,9 @@ export const Route = createFileRoute('/app/cluster')({ function RouteComponent() { const { id } = Route.useSearch(); - const cluster = useCommand('getClusterById', () => bindings.core.get_cluster(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 (
@@ -32,12 +32,12 @@ function RouteComponent() { links={{ 'Cluster Settings': [ // if someone has a better way to do this, please let me know - [, 'Overview', `/?id=${id}`], - (isModded ? [, 'Mods', `/mods?id=${id}`] : undefined), - [, 'Screenshots', `/screenshots?id=${id}`], - [, 'Worlds', `/worlds?id=${id}`], - [, 'Logs', `/logs?id=${id}`], - [, 'Game Settings', `/settings?id=${id}`], + [, 'Overview', `/?id=${id}`], + (isModded ? [, 'Mods', `/mods?id=${id}`] : undefined), + [, 'Screenshots', `/screenshots?id=${id}`], + [, 'Worlds', `/worlds?id=${id}`], + [, 'Logs', `/logs?id=${id}`], + [, 'Game Settings', `/settings?id=${id}`], ], }} /> diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx index f90d6f7c..9353c645 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx @@ -30,13 +30,16 @@ function RouteComponent() { ); } -function ScreenshotEntry(props: { path: string; cluster_path: string }) { +function ScreenshotEntry({ + path, +}: { + path: string; + cluster_path: string; +}) { function onClick() { // screen shot viewer overlay } - const { path } = props; - return (
bindings.core.get_cluster(Number(id.toString()) as unknown as bigint)); - const _result = useCommand('getProfileOrDefault', () => bindings.core.get_profile_or_default(cluster.data?.name as string), { + 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, }); diff --git a/apps/onelauncher/frontend/src/routes/app/index.tsx b/apps/onelauncher/frontend/src/routes/app/index.tsx index 2f18576c..b46af92d 100644 --- a/apps/onelauncher/frontend/src/routes/app/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/index.tsx @@ -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 (
@@ -49,7 +49,7 @@ function Banner() { */ // const cluster = useRecentCluster(); // const launch = useLaunchCluster(() => cluster()?.uuid); - const navigate = useNavigate(); + // const navigate = useNavigate(); return (
@@ -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 (
@@ -191,7 +194,7 @@ function ClusterGroup(props: ClusterGroupProps) { className="h-full w-full" >
- {props.clusters?.map(data => ( + {clusters?.map(data => ( ))}
@@ -200,11 +203,16 @@ function ClusterGroup(props: ClusterGroupProps) { ); } -function ClusterCard(props: Model) { +function ClusterCard({ + id, + name, + mc_loader, + mc_version, +}: Model) { return (
-

{props.name}

+

{name}

- {props.mc_loader} + {mc_loader} {' '} - {props.mc_version} + {mc_version} {/* {' '} {props.packages.mods && `• ${props.mods} mods`} */}

diff --git a/apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx b/apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx index 7706d189..21056bd4 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/accounts.tsx @@ -18,8 +18,8 @@ interface Account { } function RouteComponent() { - const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.core.get_users); - const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.core.get_default_user(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.core.set_default_user(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.core.remove_user(account.id), + () => bindings.core.removeUser(account.id), { enabled: false, subscribed: false, diff --git a/apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx b/apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx index e3ad63ed..784ee6f1 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/appearance.tsx @@ -77,9 +77,12 @@ interface ThemeCardProps { setTheme: (merged: string) => void; }; -function ThemeCard(props: ThemeCardProps) { +function ThemeCard({ + themeSelector, + setTheme, +}: ThemeCardProps) { return ( -
props.setTheme(props.themeSelector)}> +
setTheme(themeSelector)}> diff --git a/apps/onelauncher/frontend/src/routes/app/settings/route.tsx b/apps/onelauncher/frontend/src/routes/app/settings/route.tsx index f01d384e..3ebb7cc6 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/route.tsx @@ -103,8 +103,7 @@ function Sidebar(props: SidebarProps) {
- ), - )} + ))}
); } diff --git a/apps/onelauncher/frontend/src/routes/onboarding/complete.tsx b/apps/onelauncher/frontend/src/routes/onboarding/complete.tsx index 264e3687..1ce8de61 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/complete.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/complete.tsx @@ -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
Hello "/onboarding/complete"!
+ return
Hello "/onboarding/complete"!
; } diff --git a/apps/onelauncher/frontend/src/routes/onboarding/index.tsx b/apps/onelauncher/frontend/src/routes/onboarding/index.tsx index e600c137..88ec7cf2 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/index.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/index.tsx @@ -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 ( -
-

- Welcome to - {' '} - OneLauncher -

+ return ( +
+

+ Welcome to + {' '} + OneLauncher +

-

A powerful yet easy to use launcher for Minecraft.

-
- ) +

A powerful yet easy to use launcher for Minecraft.

+
+ ); } diff --git a/apps/onelauncher/frontend/src/routes/onboarding/login.tsx b/apps/onelauncher/frontend/src/routes/onboarding/login.tsx index abefd2bf..1e1d96fc 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/login.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/login.tsx @@ -13,7 +13,7 @@ export const Route = createFileRoute('/onboarding/login')({ function RouteComponent() { const [profile, setProfile] = useState(); const [errorMessage, setErrorMessage] = useState(''); - const result = useCommand('beginMsFlow', bindings.core.open_msa_login, { + const result = useCommand('openMsaLogin', bindings.core.openMsaLogin, { enabled: false, subscribed: false, }); diff --git a/apps/onelauncher/frontend/src/routes/onboarding/route.tsx b/apps/onelauncher/frontend/src/routes/onboarding/route.tsx index 9c384732..e7cc7ef0 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/route.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/route.tsx @@ -105,9 +105,12 @@ export interface OnboardingStepProps { children: JSX.Element; } -export function OnboardingStep(props: OnboardingStepProps) { - const { illustration, title, paragraph, children } = props; - +export function OnboardingStep({ + title, + paragraph, + illustration, + children, +}: OnboardingStepProps) { return (
diff --git a/apps/onelauncher/frontend/src/types/global.d.ts b/apps/onelauncher/frontend/src/types/global.d.ts index d5d4d0a1..bdc8303c 100644 --- a/apps/onelauncher/frontend/src/types/global.d.ts +++ b/apps/onelauncher/frontend/src/types/global.d.ts @@ -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; + } } diff --git a/apps/onelauncher/frontend/tsconfig.json b/apps/onelauncher/frontend/tsconfig.json index 6e96a58a..6325fbce 100644 --- a/apps/onelauncher/frontend/tsconfig.json +++ b/apps/onelauncher/frontend/tsconfig.json @@ -27,5 +27,5 @@ "skipLibCheck": true, "noUncheckedSideEffectImports": true }, - "include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"] + "include": ["**/*.ts", "**/*.tsx", "vite.config.js"] } diff --git a/eslint.config.js b/eslint.config.js index c6dd7afc..5e70391c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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'] }, }, ); diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 2a63013e..fba93f28 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -70,6 +70,7 @@ whoami = { workspace = true } rand = { workspace = true } url = { workspace = true } async-trait = { workspace = true } +async-stream = { workspace = true } paste = { workspace = true } byteorder = { workspace = true } merge = { workspace = true } diff --git a/packages/core/src/api/credentials.rs b/packages/core/src/api/credentials.rs index e2229123..733d639b 100644 --- a/packages/core/src/api/credentials.rs +++ b/packages/core/src/api/credentials.rs @@ -84,3 +84,14 @@ pub async fn get_user(user: uuid::Uuid) -> LauncherResult MinecraftCredentials { + MinecraftCredentials { + id: uuid::Uuid::new_v4(), + username: "Player".to_string(), + access_token: "0".to_string(), + refresh_token: "0".to_string(), + expires: chrono::Utc::now() + chrono::Duration::days(1), + } +} \ No newline at end of file diff --git a/packages/core/src/api/game/arguments.rs b/packages/core/src/api/game/arguments.rs index c3c30264..5a75a74a 100644 --- a/packages/core/src/api/game/arguments.rs +++ b/packages/core/src/api/game/arguments.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::path::Path; -use futures::AsyncReadExt; +use futures::{pin_mut, AsyncReadExt, TryStreamExt}; use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType}; use interpulse::api::modded::SidedDataEntry; use interpulse::utils::get_path_from_artifact; @@ -156,38 +156,48 @@ pub fn processor_arguments, S: ::std::hash::BuildHasher>( } pub async fn main_class(path: String) -> LauncherResult> { - // let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?; - // let mut archive = zip::ZipArchive::new(zipfs) - // .map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?; - // let file = archive - // .by_name("META-INF/MANIFEST.MF") - // .map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?; let data = io::read(path).await?; let mut class_name: Option = None; + let reader = io::stream_zip_entries_bytes(data); - io::read_zip_entries_bytes(data, async |_, entry, entry_reader| { - if entry.dir()? { - return Ok(()); + pin_mut!(reader); + while let Ok(item) = reader.try_next().await { + let Some((index, entry, reader)) = item else { + continue; + }; + + if entry.dir().unwrap_or(true) { + continue; } - if entry.filename().as_str()? != "META-INF/MANIFEST.MF" { - return Ok(()); + if entry.filename().as_str().unwrap_or_default() != "META-INF/MANIFEST.MF" { + continue; } let mut buf = String::new(); - entry_reader.read_to_string(&mut buf).await?; + let mut entry_reader = match reader.reader_without_entry(index).await { + Ok(reader) => reader, + Err(err) => { + tracing::error!("failed to get entry reader for {}: {}", entry.filename().as_str().unwrap_or_default(), err); + continue; + } + }; + + if let Err(err) = entry_reader.read_to_string(&mut buf).await { + tracing::error!("failed to read entry {}: {}", entry.filename().as_str().unwrap_or_default(), err); + continue; + } for line in buf.lines() { let line = line.trim(); - if line.starts_with("Main-Class:") - && let Some(class) = line.split(':').nth(1) { - class_name = Some(class.to_string()); - return Ok(()); + if line.starts_with("Main-Class:") { + if let Some(class) = line.split(':').nth(1) { + class_name = Some(class.trim().to_string()); + break; } + } } - - Ok(()) - }).await?; + } Ok(class_name) } diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index 7aa07f7f..9771d4e6 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -30,7 +30,6 @@ pub async fn launch_minecraft( } prepare_cluster(cluster, force).await?; - let mut settings = get_global_profile().await; if let Some(name) = &cluster.setting_profile_name && let Some(profile) = get_profile_by_name(name).await? { diff --git a/packages/core/src/api/game/metadata.rs b/packages/core/src/api/game/metadata.rs index 5dc95cdf..23e38502 100644 --- a/packages/core/src/api/game/metadata.rs +++ b/packages/core/src/api/game/metadata.rs @@ -41,9 +41,9 @@ pub async fn download_minecraft( let asset_index = download_assets_index(version, sub_ingress, force).await?; tokio::try_join! { - download_assets(version.assets == "legacy", &asset_index, sub_ingress, force), + download_assets(version.assets == "legacy", asset_index, sub_ingress, force), download_client(version, sub_ingress, force), - download_libraries(&version.id, &version.libraries, java_arch, sub_ingress, force), + download_libraries(version.id.clone(), version.libraries.clone(), java_arch.to_string(), sub_ingress, force), }?; Ok(()) @@ -122,9 +122,6 @@ pub async fn download_assets_index( .await? .join(format!("{}.json", version.asset_index.id)); - // let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0)); - // let ingress_ref = ingress_ref.as_ref(); - // ingress_ref.map(async |i| send_ingress_ref_message(i, Some("fetching assets index")).await); let ingress_step = ingress.ingress_total().map(|i| i / 2.0).unwrap_or_default(); ingress.set_ingress_message("fetching assets index").await?; @@ -159,9 +156,9 @@ pub async fn download_assets_index( // MARK: Assets /// Downloads the assets for a given asset index, returns the number of failed downloads #[tracing::instrument(skip_all)] -pub async fn download_assets( +pub async fn download_assets<'a>( legacy: bool, - assets_index: &AssetsIndex, + assets_index: AssetsIndex, ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { @@ -178,30 +175,33 @@ pub async fn download_assets( ingress.set_ingress_message("fetching assets").await?; - let requests = stream::iter(assets_index.objects.iter().map(|(name, asset)| async { - let hash = &asset.hash; - let subhash = &hash[0..2]; - let path = if legacy { - dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR)) - } else { - dir.join(subhash).join(hash) - }; + let requests = stream::iter(assets_index.objects.into_iter().map(|(name, asset)| { + let dir = dir.clone(); + async move { + let hash = &asset.hash; + let subhash = &hash[0..2]; + let path = if legacy { + dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR)) + } else { + dir.join(subhash).join(hash) + }; - if !path.exists() || force.unwrap_or(false) { - http::download_advanced( - Method::GET, - &format!("https://resources.download.minecraft.net/{subhash}/{hash}"), - path, - None, - None, - Some((HashAlgorithm::Sha1, hash)), - ingress.ingress_sub(|total| total / len as f64).as_ref(), - ) - .await - .map(|_| ()) - } else { - // TODO: Possibly check hash? (not sure if this is a good idea here) - Ok(()) + if !path.exists() || force.unwrap_or(false) { + http::download_advanced( + Method::GET, + &format!("https://resources.download.minecraft.net/{subhash}/{hash}"), + path, + None, + None, + Some((HashAlgorithm::Sha1, hash)), + ingress.ingress_sub(|total| total / len as f64).as_ref(), + ) + .await + .map(|_| ()) + } else { + // TODO: Possibly check hash? (not sure if this is a good idea here) + Ok(()) + } } })) .buffer_unordered(Core::get().fetch_attempts.min(7)) @@ -265,9 +265,9 @@ pub async fn download_client( // MARK: Libraries /// Downloads the libraries and returns the number of failed downloads pub async fn download_libraries( - version: &str, - libraries: &[Library], - java_arch: &str, + version: String, + libraries: Vec, + java_arch: String, ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { @@ -284,88 +284,95 @@ pub async fn download_libraries( let ingress = ingress.as_ref(); ingress.set_ingress_message("fetching libraries").await?; - let requests = stream::iter(libraries.iter().map(|lib| async { - if let Some(rules) = &lib.rules - && !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) { - tracing::debug!("skipping library {} due to rules", lib.name); - return Ok::<(), LauncherError>(()); - } + let requests = stream::iter(libraries.into_iter().map(|lib| { + let lib_dir = lib_dir.clone(); + let natives_dest = natives_dest.clone(); + let java_arch = java_arch.clone(); - if !lib.downloadable { - tracing::debug!("skipping library {} due to downloadability", lib.name); - return Ok(()); - } - - let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?; - let path = lib_dir.join(&artifact_path); - - if path.exists() && !force.unwrap_or(false) { - tracing::debug!("library {} is installed, skipping", &lib.name); - return Ok(()); - } - - tokio::try_join! { - async { - if let Some(interpulse::api::minecraft::LibraryDownloads { - artifact: Some(ref artifact), .. - }) = lib.downloads - && !artifact.url.is_empty() { - http::download( - Method::GET, - &artifact.url, - &path, - Some((HashAlgorithm::Sha1, &artifact.sha1)), - ingress, - ).await?; - - tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); - return Ok::<_, LauncherError>(()); - } - - let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat(); - http::download( - Method::GET, - &url, - &path, - None, - ingress, - ).await?; - - tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); - Ok::<_, LauncherError>(()) - }, - async { - if let Some((os_key, classifiers)) = None.or_else(|| Some(( - lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?, - lib.downloads.as_ref()?.classifiers.as_ref()? - ))) { - let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH); - if let Some(native) = classifiers.get(&parsed) { - tracing::trace!("found native library {}", &lib.name); - - let data = http::fetch_advanced( - Method::GET, - &native.url, - None, - Some((HashAlgorithm::Sha1, &native.sha1)), - None, - ingress, - ).await?; - - io::unzip_bytes_filtered( - data.to_vec(), - Some(|name: &str| !name.starts_with("META-INF")), - &natives_dest - ).await?; - tracing::trace!("extracted native {} to path {:?}", &lib.name, &natives_dest); - } + async move { + if let Some(rules) = &lib.rules + && !super::rules::validate_rules(rules, &java_arch, lib.natives.is_some()) { + tracing::debug!("skipping library {} due to rules", lib.name); + return Ok::<(), LauncherError>(()); } - Ok(()) + if !lib.downloadable { + tracing::debug!("skipping library {} due to downloadability", lib.name); + return Ok(()); } - }?; - Ok(()) + let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?; + let path = lib_dir.join(&artifact_path); + + if path.exists() && !force.unwrap_or(false) { + tracing::debug!("library {} is installed, skipping", &lib.name); + return Ok(()); + } + + tokio::try_join! { + async { + if let Some(interpulse::api::minecraft::LibraryDownloads { + artifact: Some(ref artifact), .. + }) = lib.downloads + && !artifact.url.is_empty() { + http::download( + Method::GET, + &artifact.url, + &path, + Some((HashAlgorithm::Sha1, &artifact.sha1)), + ingress, + ).await?; + + tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); + return Ok::<_, LauncherError>(()); + } + + let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat(); + http::download( + Method::GET, + &url, + &path, + None, + ingress, + ).await?; + + tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); + Ok::<_, LauncherError>(()) + }, + async { + if let Some((os_key, classifiers)) = None.or_else(|| Some(( + lib.natives.as_ref()?.get(&Os::native_arch(&java_arch))?, + lib.downloads.as_ref()?.classifiers.as_ref()? + ))) { + let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH); + if let Some(native) = classifiers.get(&parsed) { + tracing::trace!("found native library {}", &lib.name); + + let data = http::fetch_advanced( + Method::GET, + &native.url, + None, + Some((HashAlgorithm::Sha1, &native.sha1)), + None, + ingress, + ).await?; + + io::unzip_bytes_filtered( + data.to_vec(), + Some(|name: &str| !name.starts_with("META-INF")), + &natives_dest + ).await?; + + tracing::trace!("extracted native {} to path {:?}", &lib.name, &natives_dest); + } + } + + Ok(()) + } + }?; + + Ok(()) + } })).buffer_unordered(7).collect::>(); let mut failed = 0; diff --git a/packages/core/src/api/processes.rs b/packages/core/src/api/processes.rs index ef21db3c..c4ecccf4 100644 --- a/packages/core/src/api/processes.rs +++ b/packages/core/src/api/processes.rs @@ -4,6 +4,7 @@ use crate::store::processes::Process; #[onelauncher_macro::specta] #[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] pub enum ProcessPayload { Starting { command: String, diff --git a/packages/core/src/api/proxy/impl/proxy_tauri.rs b/packages/core/src/api/proxy/impl/proxy_tauri.rs index edd8b7e7..f38d0558 100644 --- a/packages/core/src/api/proxy/impl/proxy_tauri.rs +++ b/packages/core/src/api/proxy/impl/proxy_tauri.rs @@ -30,7 +30,11 @@ impl LauncherProxy for ProxyTauri { } } - Ok(self.emitter.send_event(event)?) + Ok(match event { + LauncherEvent::Ingress(ingress) => self.emitter.ingress(ingress), + LauncherEvent::Message(message) => self.emitter.message(message), + LauncherEvent::Process(process) => self.emitter.process(process), + }?) } #[tracing::instrument] diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index a05aca35..770563e6 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -9,21 +9,47 @@ use onelauncher_entity::prelude::model::*; pub trait TauriLauncherApi { // Clusters + #[taurpc(alias = "getClusters")] async fn get_clusters() -> LauncherResult>; - async fn get_cluster(id: ClusterId) -> LauncherResult>; + + #[taurpc(alias = "getClusterById")] + async fn get_cluster_by_id(id: ClusterId) -> LauncherResult>; + + #[taurpc(alias = "removeCluster")] async fn remove_cluster(id: ClusterId) -> LauncherResult<()>; + + #[taurpc(alias = "createCluster")] async fn create_cluster(options: CreateCluster) -> LauncherResult; + #[taurpc(alias = "launchCluster")] + async fn launch_cluster(id: ClusterId, uuid: Option) -> LauncherResult<()>; + + // Setting Profiles + #[taurpc(alias = "getProfileOrDefault")] async fn get_profile_or_default(name: Option) -> LauncherResult; + + #[taurpc(alias = "getGlobalProfile")] async fn get_global_profile() -> SettingsProfile; + // Users + #[taurpc(alias = "getUsers")] async fn get_users() -> LauncherResult>; + + #[taurpc(alias = "getUser")] async fn get_user(uuid: uuid::Uuid) -> LauncherResult>; + + #[taurpc(alias = "removeUser")] async fn remove_user(uuid: uuid::Uuid) -> LauncherResult<()>; + + #[taurpc(alias = "getDefaultUser")] async fn get_default_user(fallback: Option) -> LauncherResult>; + + #[taurpc(alias = "setDefaultUser")] async fn set_default_user(uuid: Option) -> LauncherResult<()>; + + #[taurpc(alias = "openMsaLogin")] async fn open_msa_login(app_handle: AppHandle) -> LauncherResult>; } @@ -48,7 +74,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl { api::cluster::dao::get_all_clusters().await } - async fn get_cluster(self, id: ClusterId) -> LauncherResult> { + async fn get_cluster_by_id(self, id: ClusterId) -> LauncherResult> { api::cluster::dao::get_cluster_by_id(id).await } @@ -69,6 +95,25 @@ impl TauriLauncherApi for TauriLauncherApiImpl { Ok(cluster) } + async fn launch_cluster(self, id: ClusterId, uuid: Option) -> LauncherResult<()> { + let mut cluster = api::cluster::dao::get_cluster_by_id(id).await? + .ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?; + + let uuid = match uuid { + Some(uuid) => uuid, + None => api::credentials::get_default_user().await?.ok_or_else(|| anyhow::anyhow!("no default user set"))?, + }; + + let user = api::credentials::get_user(uuid).await? + .ok_or_else(|| anyhow::anyhow!("user with uuid {} not found", uuid))?; + + // let user = api::credentials::get_fake_user(); + + let _ = api::game::launch::launch_minecraft(&mut cluster, user, None).await?; + + Ok(()) + } + async fn get_profile_or_default(self, name: Option) -> LauncherResult { api::setting_profiles::dao::get_profile_or_default(name.as_ref()).await } @@ -164,4 +209,3 @@ impl TauriLauncherApi for TauriLauncherApiImpl { } } - diff --git a/packages/core/src/api/tauri/events.rs b/packages/core/src/api/tauri/events.rs index 6ff19fab..26170a47 100644 --- a/packages/core/src/api/tauri/events.rs +++ b/packages/core/src/api/tauri/events.rs @@ -1,9 +1,16 @@ -use crate::api::proxy::event::LauncherEvent; - -#[taurpc::procedures(event_trigger = LauncherEventEmitter)] +#[taurpc::procedures(path = "events", event_trigger = LauncherEventEmitter)] pub trait TauriLauncherEventApi { #[taurpc(event)] - async fn send_event(event: LauncherEvent); + #[taurpc(alias = "ingress")] + async fn ingress(event: crate::api::ingress::IngressPayload); + + #[taurpc(event)] + #[taurpc(alias = "message")] + async fn message(event: crate::api::proxy::message::MessagePayload); + + #[taurpc(event)] + #[taurpc(alias = "process")] + async fn process(event: crate::api::processes::ProcessPayload); } #[taurpc::ipc_type] diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5b64e6cb..df088b52 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -30,4 +30,4 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP tracing::info!("Core initialized successfully"); Ok(()) -} +} \ No newline at end of file diff --git a/packages/core/src/utils/crypto.rs b/packages/core/src/utils/crypto.rs index 8020dab4..1daa5134 100644 --- a/packages/core/src/utils/crypto.rs +++ b/packages/core/src/utils/crypto.rs @@ -32,76 +32,54 @@ pub enum HashAlgorithm { const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB impl HashAlgorithm { - pub fn hasher_sync(&self, closure: F) -> Result - where - F: Fn(&mut (dyn DynDigest)) -> Result<(), IOError>, - { - let mut hasher = get_inner(self); - closure(&mut *hasher)?; - Ok(to_hex(&hasher.finalize())) - } - - pub async fn hasher(&self, closure: F) -> Result - where - F: AsyncFn(&mut (dyn DynDigest + Send + Sync)) -> Result<(), IOError> + Send + Sync, - { - let mut hasher = get_inner(self); - closure(&mut *hasher).await?; - Ok(to_hex(&hasher.finalize())) - } - pub async fn hash(&self, data: &[u8]) -> Result { - self.hasher(async move |hasher| { - hasher.update(data); - Ok(()) - }).await + let mut hasher = get_inner(self); + hasher.update(data); + Ok(to_hex(&hasher.finalize())) } pub fn hash_sync(&self, data: &[u8]) -> Result { - self.hasher_sync(move |hasher| { - hasher.update(data); - Ok(()) - }) + let mut hasher = get_inner(self); + hasher.update(data); + Ok(to_hex(&hasher.finalize())) } pub async fn hash_file(&self, path: impl AsRef) -> Result { let path = path.as_ref(); - self.hasher(async |hasher: &mut (dyn DynDigest + Send + Sync)| { - let file = tokio::fs::File::open(path).await?; - let mut reader = tokio::io::BufReader::new(file); - let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); - loop { - let bytes_read = reader.read(&mut buffer).await?; - if bytes_read == 0 { - break; - } + let mut hasher = get_inner(self); - hasher.update(&buffer[..bytes_read]); + let file = tokio::fs::File::open(path).await?; + let mut reader = tokio::io::BufReader::new(file); + + let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); + loop { + let bytes_read = reader.read(&mut buffer).await?; + if bytes_read == 0 { + break; } - Ok(()) - }).await + hasher.update(&buffer[..bytes_read]); + } + + Ok(to_hex(&hasher.finalize())) } pub fn hash_file_sync(&self, path: impl AsRef) -> Result { - self.hasher_sync(|hasher: &mut dyn DynDigest| { - let path = path.as_ref(); - let file = std::fs::File::open(path)?; - let mut reader = std::io::BufReader::new(file); + let path = path.as_ref(); + let mut hasher = get_inner(self); + let mut file = std::fs::File::open(path)?; - let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); - loop { - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); + let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; } - Ok(()) - }) + hasher.update(&buffer[..bytes_read]); + } + Ok(to_hex(&hasher.finalize())) } } diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index 1342b91f..cb376e2d 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -1,8 +1,10 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use async_compression::tokio::bufread::GzipDecoder; +use async_stream::try_stream; use async_zip::StoredZipEntry; use async_zip::base::read::WithoutEntry; +use futures::{pin_mut, Stream, TryStreamExt}; use serde::Serialize; use serde::de::DeserializeOwned; use tempfile::TempDir; @@ -278,6 +280,24 @@ where Ok(()) } +/// Reads a zip archive from a byte array and returns a stream of entries. +pub fn stream_zip_entries_bytes( + data: Vec, +) -> impl Stream +), IOError>> { + try_stream! { + let reader = Arc::new(async_zip::base::read::mem::ZipFileReader::new(data).await?); + let entries = reader.file().entries(); + + for index in 0..entries.len() { + let entry = entries.get(index).expect("expected more zip entries"); + + yield (index, entry.clone(), reader.clone()); + } + } +} + /// Unzips a zip archive from a byte array pub async fn unzip_bytes( data: Vec, @@ -289,15 +309,18 @@ pub async fn unzip_bytes( /// Unzips a zip archive from a byte array pub async fn unzip_bytes_filtered( data: Vec, - filter_entries: Option bool>, + filter_entries: Option bool + Send + Sync>, dest_path: impl AsRef, ) -> Result<(), IOError> { - read_zip_entries_bytes(data, async |_, entry, entry_reader| { + let stream = stream_zip_entries_bytes(data); + pin_mut!(stream); + + while let Some((index, entry, reader)) = stream.try_next().await? { let file_name = entry.filename().as_str()?; if let Some(filter) = &filter_entries && !filter(file_name) { - return Ok(()); + continue; } let path = dest_path.as_ref().join(sanitize_path(file_name)); @@ -311,15 +334,14 @@ pub async fn unzip_bytes_filtered( create_dir_all(parent).await?; } + let entry_reader = reader.reader_without_entry(index).await?; + let file = tokio::fs::File::create(&path).await?; let writer = tokio::io::BufWriter::new(file); futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?; } - - Ok(()) - }) - .await?; + } Ok(()) } diff --git a/packages/web_common/src/components/AnimatedOutlet.tsx b/packages/web_common/src/components/AnimatedOutlet.tsx index 3a28c690..2c6cecc5 100644 --- a/packages/web_common/src/components/AnimatedOutlet.tsx +++ b/packages/web_common/src/components/AnimatedOutlet.tsx @@ -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 ( diff --git a/packages/web_common/src/hooks/useCommand.ts b/packages/web_common/src/hooks/useCommand.ts index 01c5d0a7..752218db 100644 --- a/packages/web_common/src/hooks/useCommand.ts +++ b/packages/web_common/src/hooks/useCommand.ts @@ -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(cacheKey: CacheKey | (string & {}), command: () => Promise, options?: Omit, 'queryKey' | 'queryFn'>): UseQueryResult { +type RegisterConfig = Register extends { + commands: infer Keys; +} ? Keys : never; + +export function useCommand(cacheKey: RegisterConfig[number] | (string & {}) | false, command: () => Promise, options?: Omit, 'queryKey' | 'queryFn'>): UseQueryResult { return useQuery({ - queryKey: [cacheKey], + queryKey: cacheKey === false ? [] : [cacheKey], queryFn: async () => { try { const result = await command(); @@ -24,5 +31,3 @@ export function useCommand(cacheKey: CacheKey | (string & {}), command: () => ...options, }); } - -// export useCommand; diff --git a/packages/web_common/src/index.ts b/packages/web_common/src/index.ts index fd70c425..10b7a1b5 100644 --- a/packages/web_common/src/index.ts +++ b/packages/web_common/src/index.ts @@ -1,2 +1,3 @@ export * from './hooks'; +export type * from './types/global'; export * from './utils';