Merge branch 'oneclient/main' of https://github.com/Polyfrost/OneLauncher into refactor/frontend

This commit is contained in:
emirsassan 2025-06-07 13:32:58 +03:00
commit b7cac50020
46 changed files with 494 additions and 347 deletions

View file

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

1
Cargo.lock generated
View file

@ -3388,6 +3388,7 @@ version = "0.2.0-alpha.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
"async-stream",
"async-trait", "async-trait",
"async_zip", "async_zip",
"base64 0.22.1", "base64 0.22.1",

View file

@ -160,6 +160,7 @@ sea-orm-migration = { version = "1.1", features = [
# utils # utils
anyhow = { version = "1" } anyhow = { version = "1" }
async-trait = { version = "=0.1.88" } async-trait = { version = "=0.1.88" }
async-stream = { version = "=0.3.6" }
cc = { version = "1" } cc = { version = "1" }
discord-rich-presence = { version = "=0.2.5" } discord-rich-presence = { version = "=0.2.5" }
futures = { version = "0.3" } futures = { version = "0.3" }

View file

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

View file

@ -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 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 type MessageLevel = 'Info' | 'Warn' | 'Error';
export interface MessagePayload { level: MessageLevel; message: string } 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 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 } 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 } 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 { 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> }; events: { ingress: (event: IngressPayload) => Promise<void>; message: (event: MessagePayload) => Promise<void>; process: (event: ProcessPayload) => Promise<void> };
'oneclient': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> }; core: { getClusters: () => Promise<Array<Model>>; getClusterById: (id: bigint) => Promise<Model | null>; removeCluster: (id: bigint) => Promise<null>; createCluster: (options: CreateCluster) => Promise<Model>; launchCluster: (id: bigint, uuid: string | null) => Promise<null>; 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> };
'': { send_event: (event: LauncherEvent) => Promise<void> }; oneclient: { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> };
} }
export type { InferCommandOutput }; export type { InferCommandOutput };

View file

@ -1,15 +1,36 @@
import { bindings } from '@/main';
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { Button } from "@onelauncher/common/components";
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: App, component: App,
}); });
function 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 ( return (
<div className="text-center"> <div className="text-center">
<h1>test</h1> <h1>test</h1>
<Button>Click Me!</Button> <Button onClick={onClick}>Click Me!</Button>
</div> </div>
); );
} }

View file

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

View file

@ -117,12 +117,16 @@ set_default_user: (uuid: string | null) => Promise<null>,
open_msa_login: () => Promise<MinecraftCredentials | null> }, open_msa_login: () => Promise<MinecraftCredentials | null> },
'': { send_event: (event: LauncherEvent) => Promise<void> } }; '': { send_event: (event: LauncherEvent) => Promise<void> } };
<<<<<<< HEAD
<<<<<<< Updated upstream <<<<<<< 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 = { '': '{"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 { export interface Router {
'onelauncher': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> }; '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> }; '': { 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> };
} }
======= =======
>>>>>>> Stashed changes >>>>>>> Stashed changes

View file

@ -1,15 +1,15 @@
import type { ToOptions } from '@tanstack/react-router'; import type { ToOptions } from '@tanstack/react-router';
import {useCommand} from '@onelauncher/common';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Menu } from '@onelauncher/common/components';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { Bell01Icon, Cloud01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-react'; import { Bell01Icon, Cloud01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-react';
import { MenuTrigger } from 'react-aria-components'; import { MenuTrigger } from 'react-aria-components';
import { Button, Menu } from '@onelauncher/common/components';
import PlayerHead from './content/PlayerHead'; import PlayerHead from './content/PlayerHead';
import AccountPopup from './overlay/popups/AccountPopup'; import AccountPopup from './overlay/popups/AccountPopup';
function Navbar() { function Navbar() {
const defaultUser = useCommand('getDefaultUser', () => bindings.core.get_default_user(false)); const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
return ( return (
<div className="h-15 min-h-15 flex flex-row items-center *:flex-1"> <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 = { export interface SettingsRowProps {
title: JSX.Element | string; title: JSX.Element | string;
description: JSX.Element | string; description: JSX.Element | string;
icon: JSX.Element; icon: JSX.Element;
disabled?: boolean; disabled?: boolean;
children?: JSX.Element; children?: JSX.Element;
}; }
function SettingsRow(props: SettingsRowProps) { function SettingsRow({ disabled, description, icon, title, children }: SettingsRowProps) {
return ( return (
<div <div
className={` className={`
flex flex-row items-center gap-3.5 rounded-xl p-4 my-2 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"> <div className="flex h-8 w-8 items-center justify-center">
{props.icon} {icon}
</div> </div>
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<h3 className="text-lg capitalize leading-tight">{props.title}</h3> <h3 className="text-lg capitalize leading-tight">{title}</h3>
<p className="text-sm text-fg-secondary">{props.description}</p> <p className="text-sm text-fg-secondary">{description}</p>
</div> </div>
<div className="flex h-9 flex-row items-center gap-2"> <div className="flex h-9 flex-row items-center gap-2">
{props.children} {children}
</div> </div>
</div> </div>
); );
} }
interface SettingsRowHeaderProps { interface SettingsRowHeaderProps {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
} }
SettingsRow.Header = ({ className, children }: SettingsRowHeaderProps) => { SettingsRow.Header = ({ className, children }: SettingsRowHeaderProps) => {
const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`; const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`;
return <h3 className={headerClasses}>{children}</h3>; return <h3 className={headerClasses}>{children}</h3>;
}; };
export default SettingsRow; export default SettingsRow;

View file

@ -1,4 +1,5 @@
import type { ImgHTMLAttributes, JSX } from 'react'; import type { ImgHTMLAttributes, JSX } from 'react';
import { Show } from '@onelauncher/common/components';
import CurseforgeIcon from '../../assets/logos/curseforge.svg'; import CurseforgeIcon from '../../assets/logos/curseforge.svg';
import AtLauncherIcon from '../../assets/logos/launchers/atlauncher.svg'; import AtLauncherIcon from '../../assets/logos/launchers/atlauncher.svg';
import FTBIcon from '../../assets/logos/launchers/ftb.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 TLauncherImage from '../../assets/logos/launchers/tlauncher.png';
import UnknownImage from '../../assets/logos/minecraft.png'; import UnknownImage from '../../assets/logos/minecraft.png';
import ModrinthIcon from '../../assets/logos/modrinth.svg'; import ModrinthIcon from '../../assets/logos/modrinth.svg';
import { Show } from '../base/Show';
type ElementType = (props: any) => JSX.Element; type ElementType = (props: any) => JSX.Element;
@ -45,8 +45,8 @@ type LauncherIconProps = ImgHTMLAttributes<HTMLImageElement> & {
}; };
function LauncherIcon({ function LauncherIcon({
launcher, launcher,
...props ...props
}: LauncherIconProps) { }: LauncherIconProps) {
return ( return (
<Show <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 FabricImage from '@/assets/logos/loaders/fabric.png';
import ForgeImage from '@/assets/logos/loaders/forge.png'; import ForgeImage from '@/assets/logos/loaders/forge.png';
import NeoForgeImage from '@/assets/logos/loaders/neoforge.png'; import NeoForgeImage from '@/assets/logos/loaders/neoforge.png';
import QuiltImage from '@/assets/logos/loaders/quilt.png'; import QuiltImage from '@/assets/logos/loaders/quilt.png';
import VanillaImage from '@/assets/logos/minecraft.png'; import VanillaImage from '@/assets/logos/minecraft.png';
import type { RefAttributes } from 'react';
export function getLoaderLogoSrc(loader: Model | GameLoader): string { 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> = { const mapping: Record<GameLoader, string> = {
vanilla: VanillaImage, vanilla: VanillaImage,
@ -21,17 +21,21 @@ export function getLoaderLogoSrc(loader: Model | GameLoader): string {
return mapping[loaderName]; return mapping[loaderName];
} }
type LoaderIconProp = RefAttributes<HTMLImageElement> & { type LoaderIconProp = ImgHTMLAttributes<HTMLImageElement> & RefAttributes<HTMLImageElement> & {
loader: GameLoader | undefined; loader: GameLoader | undefined;
className?: string;
}; };
function LoaderIcon(props: LoaderIconProp) { function LoaderIcon({
loader,
src: _src,
alt: _alt,
...rest
}: LoaderIconProp) {
return ( return (
<img <img
{...props} {...rest}
alt={`${props.loader}'s logo`} alt={`${loader}'s logo`}
src={props.loader ? getLoaderLogoSrc(props.loader) : VanillaImage} src={loader ? getLoaderLogoSrc(loader) : VanillaImage}
/> />
); );
} }

View file

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

View file

@ -8,11 +8,11 @@ import { Separator } from 'react-aria-components';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
function AccountPopup() { function AccountPopup() {
const users = useCommand('getUsers', bindings.core.get_users); const users = useCommand('getUsers', bindings.core.getUsers);
const defaultUser = useCommand('getDefaultUser', () => bindings.core.get_default_user(false)); const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
const setDefaultUser = (user: MinecraftCredentials) => { const setDefaultUser = (user: MinecraftCredentials) => {
bindings.core.set_default_user(user.id); bindings.core.setDefaultUser(user.id);
}; };
return ( return (

View file

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

View file

@ -9,6 +9,7 @@ import { Button, Show, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react'; import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import Sidebar from '../settings/route'; import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/')({ export const Route = createFileRoute('/app/cluster/')({
@ -21,7 +22,7 @@ function RouteComponent() {
const [newName, setNewName] = useState<string>(''); const [newName, setNewName] = useState<string>('');
// dumbass fix ik // 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 ( return (
<Sidebar.Page> <Sidebar.Page>
@ -102,7 +103,11 @@ interface BannerProps {
refetch: () => void; refetch: () => void;
} }
function Banner(props: BannerProps) { function Banner({
cluster,
editMode,
setNewName,
}: BannerProps) {
// async function launchFilePicker() { // async function launchFilePicker() {
// const selected = await dialog.open({ // const selected = await dialog.open({
// multiple: false, // multiple: false,
@ -162,7 +167,7 @@ function Banner(props: BannerProps) {
<div className="flex flex-1 flex-row"> <div className="flex flex-1 flex-row">
<div <div
className={`flex flex-1 flex-col items-start justify-between ${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"> <span className="flex flex-row items-center gap-x-1">
<LoaderIcon <LoaderIcon
@ -174,7 +179,7 @@ function Banner(props: BannerProps) {
{cluster?.mc_loader_version && <span>{cluster.mc_loader_version}</span>} {cluster?.mc_loader_version && <span>{cluster.mc_loader_version}</span>}
</span> </span>
<span <span
className={`text-xs text-fg-secondary ${editMode ? 'text-fg-secondary-disabled' : ''}`} className={twMerge(`text-xs text-fg-secondary`, editMode && 'text-fg-secondary-disabled')}
> >
Played for Played for
{' '} {' '}

View file

@ -20,9 +20,9 @@ export const Route = createFileRoute('/app/cluster')({
function RouteComponent() { function RouteComponent() {
const { id } = Route.useSearch(); 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 ( return (
<div className="h-full flex flex-row overflow-hidden"> <div className="h-full flex flex-row overflow-hidden">
@ -32,12 +32,12 @@ function RouteComponent() {
links={{ links={{
'Cluster Settings': [ 'Cluster Settings': [
// if someone has a better way to do this, please let me know // if someone has a better way to do this, please let me know
[<EyeIcon key="overview" />, 'Overview', `/?id=${id}`], [<EyeIcon />, 'Overview', `/?id=${id}`],
(isModded ? [<PackagePlusIcon key="mods" />, 'Mods', `/mods?id=${id}`] : undefined), (isModded ? [<PackagePlusIcon />, 'Mods', `/mods?id=${id}`] : undefined),
[<Image03Icon key="screenshots" />, 'Screenshots', `/screenshots?id=${id}`], [<Image03Icon />, 'Screenshots', `/screenshots?id=${id}`],
[<Globe04Icon key="worlds" />, 'Worlds', `/worlds?id=${id}`], [<Globe04Icon />, 'Worlds', `/worlds?id=${id}`],
[<File06Icon key="logs" />, 'Logs', `/logs?id=${id}`], [<File06Icon />, 'Logs', `/logs?id=${id}`],
[<Settings04Icon key="gamesettings" />, 'Game Settings', `/settings?id=${id}`], [<Settings04Icon />, 'Game Settings', `/settings?id=${id}`],
], ],
}} }}
/> />

View file

@ -30,13 +30,16 @@ function RouteComponent() {
); );
} }
function ScreenshotEntry(props: { path: string; cluster_path: string }) { function ScreenshotEntry({
path,
}: {
path: string;
cluster_path: string;
}) {
function onClick() { function onClick() {
// screen shot viewer overlay // screen shot viewer overlay
} }
const { path } = props;
return ( return (
<div <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" 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"

View file

@ -12,8 +12,8 @@ export const Route = createFileRoute('/app/cluster/settings')({
function RouteComponent() { function RouteComponent() {
const { id } = Route.useSearch(); 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 _result = useCommand('getProfileOrDefault', () => bindings.core.get_profile_or_default(cluster.data?.name as string), { const result = useCommand('getProfileOrDefault', () => bindings.core.getProfileOrDefault(cluster.data?.name as string), {
enabled: !!cluster.data?.name, enabled: !!cluster.data?.name,
}); });

View file

@ -6,7 +6,7 @@ import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common'; import { useCommand } from '@onelauncher/common';
import { Button, TextField } from '@onelauncher/common/components'; 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 { Server01Icon } from '@untitled-theme/icons-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-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 and i am very angry rn so i will not be fixing it rn
*/ */
function RouteComponent() { function RouteComponent() {
const result = useCommand('getClusters', bindings.core.get_clusters); const result = useCommand('getClusters', bindings.core.getClusters);
return ( return (
<div className="h-full flex flex-col gap-y-4 text-fg-primary"> <div className="h-full flex flex-col gap-y-4 text-fg-primary">
@ -49,7 +49,7 @@ function Banner() {
*/ */
// const cluster = useRecentCluster(); // const cluster = useRecentCluster();
// const launch = useLaunchCluster(() => cluster()?.uuid); // const launch = useLaunchCluster(() => cluster()?.uuid);
const navigate = useNavigate(); // const navigate = useNavigate();
return ( return (
<div className="relative h-52 min-h-52 w-full overflow-hidden rounded-xl border border-component-border"> <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() { function ClusterCreate() {
const result = useCommand('createCluster', () => bindings.commands.createCluster({ const result = useCommand('createCluster', () => bindings.core.createCluster({
icon_url: 'asd', icon: 'asd',
mc_loader: 'vanilla', mc_loader: 'vanilla',
mc_version: '1.20.1', mc_version: '1.20.1',
name: 'Test Cluster', name: 'Test Cluster',
@ -177,8 +177,11 @@ interface ClusterGroupProps {
isFetching?: boolean; isFetching?: boolean;
} }
function ClusterGroup(props: ClusterGroupProps) { function ClusterGroup({
if (props.isFetching) isFetching,
clusters,
}: ClusterGroupProps) {
if (isFetching)
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" /> <div 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" className="h-full w-full"
> >
<div className="grid grid-cols-4 gap-4 max-h-96 2xl:grid-cols-6 pb-4"> <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} /> <ClusterCard key={data.id} {...data} />
))} ))}
</div> </div>
@ -200,11 +203,16 @@ function ClusterGroup(props: ClusterGroupProps) {
); );
} }
function ClusterCard(props: Model) { function ClusterCard({
id,
name,
mc_loader,
mc_version,
}: Model) {
return ( return (
<Link <Link
search={{ search={{
id: props.id, id,
}} to="/app/cluster" }} to="/app/cluster"
> >
<div <div
@ -222,11 +230,11 @@ function ClusterCard(props: Model) {
</div> </div>
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3"> <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"> <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"> <p className="h-4 text-xs">
{props.mc_loader} {mc_loader}
{' '} {' '}
{props.mc_version} {mc_version}
{/* {' '} {/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */} {props.packages.mods && `• ${props.mods} mods`} */}
</p> </p>

View file

@ -18,8 +18,8 @@ interface Account {
} }
function RouteComponent() { function RouteComponent() {
const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.core.get_users); const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.core.getUsers);
const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.core.get_default_user(false)); const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
if (usersLoading || defaultUserLoading) if (usersLoading || defaultUserLoading)
return ( return (
@ -67,7 +67,7 @@ function AccountRow({ account, isCurrent }: AccountRowProps) {
const setDefaultUserCommand = useCommand( const setDefaultUserCommand = useCommand(
'setDefaultUser', 'setDefaultUser',
() => bindings.core.set_default_user(account.id), () => bindings.core.setDefaultUser(account.id),
{ {
enabled: false, enabled: false,
subscribed: false, subscribed: false,
@ -76,7 +76,7 @@ function AccountRow({ account, isCurrent }: AccountRowProps) {
const removeUserCommand = useCommand( const removeUserCommand = useCommand(
'removeUser', 'removeUser',
() => bindings.core.remove_user(account.id), () => bindings.core.removeUser(account.id),
{ {
enabled: false, enabled: false,
subscribed: false, subscribed: false,

View file

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

View file

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

View file

@ -103,8 +103,7 @@ function Sidebar(props: SidebarProps) {
</div> </div>
</div> </div>
</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')({ export const Route = createFileRoute('/onboarding/complete')({
component: RouteComponent, component: RouteComponent,
}) });
function 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/')({ export const Route = createFileRoute('/onboarding/')({
component: RouteComponent, component: RouteComponent,
}) });
function RouteComponent() { function RouteComponent() {
return ( return (
<div className="grid grid-cols-2 h-full w-full items-center gap-x-16"> <div className="grid grid-cols-2 h-full w-full items-center gap-x-16">
<h1 className="text-6xl"> <h1 className="text-6xl">
Welcome to Welcome to
{' '} {' '}
<span className="underline underline-8 underline-brand">OneLauncher</span> <span className="underline underline-8 underline-brand">OneLauncher</span>
</h1> </h1>
<h3>A powerful yet easy to use launcher for Minecraft.</h3> <h3>A powerful yet easy to use launcher for Minecraft.</h3>
</div> </div>
) );
} }

View file

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

View file

@ -105,9 +105,12 @@ export interface OnboardingStepProps {
children: JSX.Element; children: JSX.Element;
} }
export function OnboardingStep(props: OnboardingStepProps) { export function OnboardingStep({
const { illustration, title, paragraph, children } = props; title,
paragraph,
illustration,
children,
}: OnboardingStepProps) {
return ( return (
<div className="grid grid-cols-2 h-full w-full gap-x-16"> <div className="grid grid-cols-2 h-full w-full gap-x-16">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">

View file

@ -1,3 +1,9 @@
export interface ClassNameString { import type { bindings } from '@/main';
className?: string;
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, "skipLibCheck": true,
"noUncheckedSideEffectImports": 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, react: true,
query: true, query: true,
ignores: ['*.rs', '**/migrations/**'], ignores: ['*.rs', '**/migrations/**'],
rules: {
'ts/no-use-before-define': 'off',
},
toml: { toml: {
overrides: { overrides: {
'toml/array-element-newline': ['error', 'always'], 'toml/array-element-newline': ['error', 'always'],
@ -30,7 +27,9 @@ export default defineConfig(
rules: { rules: {
'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports', 'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports',
'import/order': '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

@ -70,6 +70,7 @@ whoami = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
url = { workspace = true } url = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
async-stream = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
byteorder = { workspace = true } byteorder = { workspace = true }
merge = { workspace = true } merge = { workspace = true }

View file

@ -84,3 +84,14 @@ pub async fn get_user(user: uuid::Uuid) -> LauncherResult<Option<MinecraftCreden
Ok(user) Ok(user)
} }
/// Get a fake user for testing or offline mode.
pub fn get_fake_user() -> 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),
}
}

View file

@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use futures::AsyncReadExt; use futures::{pin_mut, AsyncReadExt, TryStreamExt};
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType}; use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
use interpulse::api::modded::SidedDataEntry; use interpulse::api::modded::SidedDataEntry;
use interpulse::utils::get_path_from_artifact; use interpulse::utils::get_path_from_artifact;
@ -156,38 +156,48 @@ pub fn processor_arguments<T: AsRef<str>, S: ::std::hash::BuildHasher>(
} }
pub async fn main_class(path: String) -> LauncherResult<Option<String>> { pub async fn main_class(path: String) -> LauncherResult<Option<String>> {
// 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 data = io::read(path).await?;
let mut class_name: Option<String> = None; let mut class_name: Option<String> = None;
let reader = io::stream_zip_entries_bytes(data);
io::read_zip_entries_bytes(data, async |_, entry, entry_reader| { pin_mut!(reader);
if entry.dir()? { while let Ok(item) = reader.try_next().await {
return Ok(()); let Some((index, entry, reader)) = item else {
continue;
};
if entry.dir().unwrap_or(true) {
continue;
} }
if entry.filename().as_str()? != "META-INF/MANIFEST.MF" { if entry.filename().as_str().unwrap_or_default() != "META-INF/MANIFEST.MF" {
return Ok(()); continue;
} }
let mut buf = String::new(); 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() { for line in buf.lines() {
let line = line.trim(); let line = line.trim();
if line.starts_with("Main-Class:") if line.starts_with("Main-Class:") {
&& let Some(class) = line.split(':').nth(1) { if let Some(class) = line.split(':').nth(1) {
class_name = Some(class.to_string()); class_name = Some(class.trim().to_string());
return Ok(()); break;
} }
}
} }
}
Ok(())
}).await?;
Ok(class_name) Ok(class_name)
} }

View file

@ -30,7 +30,6 @@ pub async fn launch_minecraft(
} }
prepare_cluster(cluster, force).await?; prepare_cluster(cluster, force).await?;
let mut settings = get_global_profile().await; let mut settings = get_global_profile().await;
if let Some(name) = &cluster.setting_profile_name if let Some(name) = &cluster.setting_profile_name
&& let Some(profile) = get_profile_by_name(name).await? { && let Some(profile) = get_profile_by_name(name).await? {

View file

@ -41,9 +41,9 @@ pub async fn download_minecraft(
let asset_index = download_assets_index(version, sub_ingress, force).await?; let asset_index = download_assets_index(version, sub_ingress, force).await?;
tokio::try_join! { 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_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(()) Ok(())
@ -122,9 +122,6 @@ pub async fn download_assets_index(
.await? .await?
.join(format!("{}.json", version.asset_index.id)); .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(); let ingress_step = ingress.ingress_total().map(|i| i / 2.0).unwrap_or_default();
ingress.set_ingress_message("fetching assets index").await?; ingress.set_ingress_message("fetching assets index").await?;
@ -159,9 +156,9 @@ pub async fn download_assets_index(
// MARK: Assets // MARK: Assets
/// Downloads the assets for a given asset index, returns the number of failed downloads /// Downloads the assets for a given asset index, returns the number of failed downloads
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn download_assets( pub async fn download_assets<'a>(
legacy: bool, legacy: bool,
assets_index: &AssetsIndex, assets_index: AssetsIndex,
ingress: Option<&SubIngress<'_>>, ingress: Option<&SubIngress<'_>>,
force: Option<bool>, force: Option<bool>,
) -> LauncherResult<usize> { ) -> LauncherResult<usize> {
@ -178,30 +175,33 @@ pub async fn download_assets(
ingress.set_ingress_message("fetching assets").await?; ingress.set_ingress_message("fetching assets").await?;
let requests = stream::iter(assets_index.objects.iter().map(|(name, asset)| async { let requests = stream::iter(assets_index.objects.into_iter().map(|(name, asset)| {
let hash = &asset.hash; let dir = dir.clone();
let subhash = &hash[0..2]; async move {
let path = if legacy { let hash = &asset.hash;
dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR)) let subhash = &hash[0..2];
} else { let path = if legacy {
dir.join(subhash).join(hash) dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR))
}; } else {
dir.join(subhash).join(hash)
};
if !path.exists() || force.unwrap_or(false) { if !path.exists() || force.unwrap_or(false) {
http::download_advanced( http::download_advanced(
Method::GET, Method::GET,
&format!("https://resources.download.minecraft.net/{subhash}/{hash}"), &format!("https://resources.download.minecraft.net/{subhash}/{hash}"),
path, path,
None, None,
None, None,
Some((HashAlgorithm::Sha1, hash)), Some((HashAlgorithm::Sha1, hash)),
ingress.ingress_sub(|total| total / len as f64).as_ref(), ingress.ingress_sub(|total| total / len as f64).as_ref(),
) )
.await .await
.map(|_| ()) .map(|_| ())
} else { } else {
// TODO: Possibly check hash? (not sure if this is a good idea here) // TODO: Possibly check hash? (not sure if this is a good idea here)
Ok(()) Ok(())
}
} }
})) }))
.buffer_unordered(Core::get().fetch_attempts.min(7)) .buffer_unordered(Core::get().fetch_attempts.min(7))
@ -265,9 +265,9 @@ pub async fn download_client(
// MARK: Libraries // MARK: Libraries
/// Downloads the libraries and returns the number of failed downloads /// Downloads the libraries and returns the number of failed downloads
pub async fn download_libraries( pub async fn download_libraries(
version: &str, version: String,
libraries: &[Library], libraries: Vec<Library>,
java_arch: &str, java_arch: String,
ingress: Option<&SubIngress<'_>>, ingress: Option<&SubIngress<'_>>,
force: Option<bool>, force: Option<bool>,
) -> LauncherResult<usize> { ) -> LauncherResult<usize> {
@ -284,88 +284,95 @@ pub async fn download_libraries(
let ingress = ingress.as_ref(); let ingress = ingress.as_ref();
ingress.set_ingress_message("fetching libraries").await?; ingress.set_ingress_message("fetching libraries").await?;
let requests = stream::iter(libraries.iter().map(|lib| async { let requests = stream::iter(libraries.into_iter().map(|lib| {
if let Some(rules) = &lib.rules let lib_dir = lib_dir.clone();
&& !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) { let natives_dest = natives_dest.clone();
tracing::debug!("skipping library {} due to rules", lib.name); let java_arch = java_arch.clone();
return Ok::<(), LauncherError>(());
}
if !lib.downloadable { async move {
tracing::debug!("skipping library {} due to downloadability", lib.name); if let Some(rules) = &lib.rules
return Ok(()); && !super::rules::validate_rules(rules, &java_arch, lib.natives.is_some()) {
} tracing::debug!("skipping library {} due to rules", lib.name);
return Ok::<(), LauncherError>(());
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(()) 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::<Vec<_>>(); })).buffer_unordered(7).collect::<Vec<_>>();
let mut failed = 0; let mut failed = 0;

View file

@ -4,6 +4,7 @@ use crate::store::processes::Process;
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum ProcessPayload { pub enum ProcessPayload {
Starting { Starting {
command: String, command: String,

View file

@ -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] #[tracing::instrument]

View file

@ -9,21 +9,47 @@ use onelauncher_entity::prelude::model::*;
pub trait TauriLauncherApi { pub trait TauriLauncherApi {
// Clusters // Clusters
#[taurpc(alias = "getClusters")]
async fn get_clusters() -> LauncherResult<Vec<Cluster>>; 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<()>; async fn remove_cluster(id: ClusterId) -> LauncherResult<()>;
#[taurpc(alias = "createCluster")]
async fn create_cluster(options: CreateCluster) -> LauncherResult<Cluster>; async fn create_cluster(options: CreateCluster) -> LauncherResult<Cluster>;
#[taurpc(alias = "launchCluster")]
async fn launch_cluster(id: ClusterId, uuid: Option<uuid::Uuid>) -> LauncherResult<()>;
// Setting Profiles // Setting Profiles
#[taurpc(alias = "getProfileOrDefault")]
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>; async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
#[taurpc(alias = "getGlobalProfile")]
async fn get_global_profile() -> SettingsProfile; async fn get_global_profile() -> SettingsProfile;
// Users // Users
#[taurpc(alias = "getUsers")]
async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>; async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>;
#[taurpc(alias = "getUser")]
async fn get_user(uuid: uuid::Uuid) -> LauncherResult<Option<MinecraftCredentials>>; async fn get_user(uuid: uuid::Uuid) -> LauncherResult<Option<MinecraftCredentials>>;
#[taurpc(alias = "removeUser")]
async fn remove_user(uuid: uuid::Uuid) -> LauncherResult<()>; async fn remove_user(uuid: uuid::Uuid) -> LauncherResult<()>;
#[taurpc(alias = "getDefaultUser")]
async fn get_default_user(fallback: Option<bool>) -> LauncherResult<Option<MinecraftCredentials>>; async fn get_default_user(fallback: Option<bool>) -> LauncherResult<Option<MinecraftCredentials>>;
#[taurpc(alias = "setDefaultUser")]
async fn set_default_user(uuid: Option<uuid::Uuid>) -> LauncherResult<()>; 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>>; async fn open_msa_login<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>>;
} }
@ -48,7 +74,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::cluster::dao::get_all_clusters().await 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 api::cluster::dao::get_cluster_by_id(id).await
} }
@ -69,6 +95,25 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(cluster) Ok(cluster)
} }
async fn launch_cluster(self, id: ClusterId, uuid: Option<uuid::Uuid>) -> 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<String>) -> LauncherResult<SettingsProfile> { async fn get_profile_or_default(self, name: Option<String>) -> LauncherResult<SettingsProfile> {
api::setting_profiles::dao::get_profile_or_default(name.as_ref()).await api::setting_profiles::dao::get_profile_or_default(name.as_ref()).await
} }
@ -164,4 +209,3 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
} }
} }

View file

@ -1,9 +1,16 @@
use crate::api::proxy::event::LauncherEvent; #[taurpc::procedures(path = "events", event_trigger = LauncherEventEmitter)]
#[taurpc::procedures(event_trigger = LauncherEventEmitter)]
pub trait TauriLauncherEventApi { pub trait TauriLauncherEventApi {
#[taurpc(event)] #[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] #[taurpc::ipc_type]

View file

@ -30,4 +30,4 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP
tracing::info!("Core initialized successfully"); tracing::info!("Core initialized successfully");
Ok(()) Ok(())
} }

View file

@ -32,76 +32,54 @@ pub enum HashAlgorithm {
const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB
impl HashAlgorithm { impl HashAlgorithm {
pub fn hasher_sync<F>(&self, closure: F) -> Result<String, IOError>
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<F>(&self, closure: F) -> Result<String, IOError>
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<String, IOError> { pub async fn hash(&self, data: &[u8]) -> Result<String, IOError> {
self.hasher(async move |hasher| { let mut hasher = get_inner(self);
hasher.update(data); hasher.update(data);
Ok(()) Ok(to_hex(&hasher.finalize()))
}).await
} }
pub fn hash_sync(&self, data: &[u8]) -> Result<String, IOError> { pub fn hash_sync(&self, data: &[u8]) -> Result<String, IOError> {
self.hasher_sync(move |hasher| { let mut hasher = get_inner(self);
hasher.update(data); hasher.update(data);
Ok(()) Ok(to_hex(&hasher.finalize()))
})
} }
pub async fn hash_file(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> { pub async fn hash_file(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
let path = path.as_ref(); 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(); let mut hasher = get_inner(self);
loop {
let bytes_read = reader.read(&mut buffer).await?;
if bytes_read == 0 {
break;
}
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(()) hasher.update(&buffer[..bytes_read]);
}).await }
Ok(to_hex(&hasher.finalize()))
} }
pub fn hash_file_sync(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> { pub fn hash_file_sync(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
self.hasher_sync(|hasher: &mut dyn DynDigest| { let path = path.as_ref();
let path = path.as_ref(); let mut hasher = get_inner(self);
let file = std::fs::File::open(path)?; let mut file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice();
loop { loop {
let bytes_read = reader.read(&mut buffer)?; let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 { if bytes_read == 0 {
break; break;
}
hasher.update(&buffer[..bytes_read]);
} }
Ok(()) hasher.update(&buffer[..bytes_read]);
}) }
Ok(to_hex(&hasher.finalize()))
} }
} }

View file

@ -1,8 +1,10 @@
use std::path::PathBuf; use std::{path::PathBuf, sync::Arc};
use async_compression::tokio::bufread::GzipDecoder; use async_compression::tokio::bufread::GzipDecoder;
use async_stream::try_stream;
use async_zip::StoredZipEntry; use async_zip::StoredZipEntry;
use async_zip::base::read::WithoutEntry; use async_zip::base::read::WithoutEntry;
use futures::{pin_mut, Stream, TryStreamExt};
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tempfile::TempDir; use tempfile::TempDir;
@ -278,6 +280,24 @@ where
Ok(()) Ok(())
} }
/// Reads a zip archive from a byte array and returns a stream of entries.
pub fn stream_zip_entries_bytes(
data: Vec<u8>,
) -> impl Stream<Item = Result<(
usize, StoredZipEntry, Arc<async_zip::base::read::mem::ZipFileReader>
), 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 /// Unzips a zip archive from a byte array
pub async fn unzip_bytes( pub async fn unzip_bytes(
data: Vec<u8>, data: Vec<u8>,
@ -289,15 +309,18 @@ pub async fn unzip_bytes(
/// Unzips a zip archive from a byte array /// Unzips a zip archive from a byte array
pub async fn unzip_bytes_filtered( pub async fn unzip_bytes_filtered(
data: Vec<u8>, data: Vec<u8>,
filter_entries: Option<impl Fn(&str) -> bool>, filter_entries: Option<impl Fn(&str) -> bool + Send + Sync>,
dest_path: impl AsRef<std::path::Path>, dest_path: impl AsRef<std::path::Path>,
) -> Result<(), IOError> { ) -> 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()?; let file_name = entry.filename().as_str()?;
if let Some(filter) = &filter_entries if let Some(filter) = &filter_entries
&& !filter(file_name) { && !filter(file_name) {
return Ok(()); continue;
} }
let path = dest_path.as_ref().join(sanitize_path(file_name)); 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?; create_dir_all(parent).await?;
} }
let entry_reader = reader.reader_without_entry(index).await?;
let file = tokio::fs::File::create(&path).await?; let file = tokio::fs::File::create(&path).await?;
let writer = tokio::io::BufWriter::new(file); let writer = tokio::io::BufWriter::new(file);
futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?; futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?;
} }
}
Ok(())
})
.await?;
Ok(()) Ok(())
} }

View file

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

View file

@ -1,11 +1,18 @@
import type { UndefinedInitialDataOptions, UseQueryResult } from '@tanstack/react-query'; import type { UndefinedInitialDataOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } 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({ return useQuery({
queryKey: [cacheKey], queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: async () => { queryFn: async () => {
try { try {
const result = await command(); const result = await command();
@ -24,5 +31,3 @@ export function useCommand<T>(cacheKey: CacheKey | (string & {}), command: () =>
...options, ...options,
}); });
} }
// export useCommand;

View file

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