feat: game startup is completed

This commit is contained in:
emirsassan 2025-06-10 21:57:53 +03:00
parent 86c3cc4f40
commit 51ba9afd49
15 changed files with 343 additions and 87 deletions

2
.vscode/launch.json vendored
View file

@ -12,7 +12,7 @@
"args": [
"build",
"--profile=dev-debug",
"--manifest-path=./apps/desktop/Cargo.toml",
"--manifest-path=./apps/onelauncher/desktop/Cargo.toml",
"--no-default-features"
],
"problemMatcher": "$rustc"

2
.vscode/tasks.json vendored
View file

@ -41,7 +41,7 @@
},
"isBackground": true,
"command": "pnpm",
"args": ["frontend", "dev", "--mode=development"],
"args": ["onelauncher:frontend", "dev", "--mode=development"],
"runOptions": {
"instanceLimit": 1
}

View file

@ -148,6 +148,7 @@ sea-orm = { version = "1.1", features = [
"with-chrono",
"with-json",
"with-uuid",
"sqlite-use-returning-for-3_35",
] }
sea-orm-migration = { version = "1.1", features = [
"sqlx-sqlite",

View file

@ -98,12 +98,9 @@ export type SettingProfileModel = { name: string; java_id: bigint | null; res: R
export type SettingsOsExtra = Record<string, never>
const ARGS_MAP = { 'core':'{"getClusters":[],"getGlobalProfile":[],"getClusterById":["id"],"getProfileOrDefault":["name"],"removeCluster":["id"],"openMsaLogin":[],"createCluster":["options"],"getUser":["uuid"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"launchCluster":["id","uuid"],"getUsers":[],"removeUser":["uuid"]}', 'onelauncher':'{"open_dev_tools":[],"return_error":[]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}' }
const ARGS_MAP = { 'events':'{"process":["event"],"message":["event"],"ingress":["event"]}', 'onelauncher':'{"return_error":[],"open_dev_tools":[]}', 'core':'{"getProfileOrDefault":["name"],"setDefaultUser":["uuid"],"launchCluster":["id","uuid"],"getClusterById":["id"],"removeCluster":["id"],"createCluster":["options"],"getUsers":[],"getUser":["uuid"],"getGlobalProfile":[],"getDefaultUser":["fallback"],"getClusters":[],"removeUser":["uuid"],"openMsaLogin":[]}' }
export type Router = { 'onelauncher': { return_error: () => Promise<null>,
open_dev_tools: () => Promise<void> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'core': { getClusters: () => Promise<Model[]>,
getClusterById: (id: bigint) => Promise<Model | null>,
removeCluster: (id: bigint) => Promise<null>,
@ -116,7 +113,10 @@ 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> } };
openMsaLogin: () => Promise<MinecraftCredentials | null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> } };
export type { InferCommandOutput }

View file

@ -0,0 +1,212 @@
import type { JSX } from 'react';
import DefaultBanner from '@/assets/images/default_banner.png';
import LauncherIcon from '@/components/content/LauncherIcon';
import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main';
import { LAUNCHER_IMPORT_TYPES } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { Button, Dropdown, TextField } from '@onelauncher/common/components';
import { Server01Icon, User03Icon } from '@untitled-theme/icons-react';
import { useCallback, useState } from 'react';
const STEPS = [
{
id: 'loader',
title: 'Select Provider',
},
{
id: 'info',
title: 'Game Setup',
},
] as const;
export function NewClusterCreate() {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [formData, setFormData] = useState({
loader: '',
clusterName: '',
clusterVersion: '',
});
const handleNext = useCallback(async () => {
if (currentStepIndex < STEPS.length - 1)
setCurrentStepIndex(currentStepIndex + 1);
}, [currentStepIndex]);
const handleBack = useCallback(() => {
if (currentStepIndex > 0)
setCurrentStepIndex(currentStepIndex - 1);
}, [currentStepIndex]);
const currentStepConfig = STEPS[currentStepIndex];
let stepContent: JSX.Element;
switch (currentStepConfig.id) {
case 'loader': {
stepContent = (
<ClusterLoader
onSelectLoader={loader => setFormData(prev => ({ ...prev, loader }))}
selectedLoader={formData.loader}
/>
);
break;
}
case 'info': {
stepContent = (
<ClusterInformation
clusterName={formData.clusterName}
clusterVersion={formData.clusterVersion}
onClusterNameChange={name => setFormData(prev => ({ ...prev, clusterName: name }))}
onClusterVersionChange={version =>
setFormData(prev => ({ ...prev, clusterVersion: version }))}
/>
);
break;
}
}
const isFirstStep = currentStepIndex === 0;
const isLastInputStep = currentStepIndex === STEPS.length - 1;
const nextButtonText = isLastInputStep ? 'Create Cluster' : 'Next';
let isNextDisabled = false;
if (currentStepConfig.id === 'loader' && !formData.loader)
isNextDisabled = true;
else if (
currentStepConfig.id === 'info'
&& (!formData.clusterName.trim() || !formData.clusterVersion.trim())
)
isNextDisabled = true;
return (
<>
<Modal.Trigger>
<Button
children="New Cluster"
/>
<Modal>
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
<div className="theme-OneLauncher-Dark relative h-25 flex">
<div className="absolute left-0 top-0 h-full w-full">
<img alt="Header Image" className="h-full w-full rounded-t-lg" src={DefaultBanner} />
</div>
<div
className="absolute left-0 top-0 h-full flex w-full flex-row items-center justify-start gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)] px-10"
>
<Server01Icon className="h-8 w-8 text-fg-primary" />
<div className="flex flex-col items-start justify-center">
<h1 className="h-10 text-fg-primary -mt-2">New Cluster</h1>
<span className="text-fg-primary">{STEPS[currentStepIndex].title}</span>
</div>
</div>
</div>
<div className="flex flex-col rounded-b-lg border border-white/5">
<div className="p-3">
{stepContent}
</div>
<div className="flex flex-row justify-end gap-x-2 p-3 pt-0">
<Button
children="Back"
color="secondary"
isDisabled={isFirstStep}
onClick={handleBack}
/>
<Button
children={nextButtonText}
color="primary"
isDisabled={isNextDisabled}
onClick={handleNext}
/>
</div>
</div>
</div>
</Modal>
</Modal.Trigger>
</>
);
}
interface ClusterLoaderProps {
selectedLoader: string;
onSelectLoader: (loader: string) => void;
}
function ClusterLoader({ selectedLoader, onSelectLoader }: ClusterLoaderProps) {
return (
<>
<div className="grid grid-cols-3 gap-2">
<ProviderCard
icon={<User03Icon />}
name="New"
selected={selectedLoader === 'new'}
setSelected={() => onSelectLoader('new')}
/>
{LAUNCHER_IMPORT_TYPES.map((data, i) => (
<ProviderCard
icon={<LauncherIcon launcher={data} />}
key={i}
name={data}
selected={selectedLoader === data}
setSelected={() => onSelectLoader(data)}
/>
))}
</div>
</>
);
}
interface ProviderCardProps {
setSelected: () => void;
selected: boolean;
icon: JSX.Element;
name: string;
}
function ProviderCard(props: ProviderCardProps) {
const { selected, setSelected, icon, name } = props;
return (
<div
className={`flex flex-col justify-center items-center gap-y-3 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg ${selected ? 'bg-component-bg' : ''}`}
onClick={() => setSelected()}
>
<div className="h-8 w-8 flex items-center justify-center [&>svg]:(w-8 h-8!)">
{icon}
</div>
<span>{name}</span>
</div>
);
}
interface ClusterInformationProps {
clusterName: string;
onClusterNameChange: (name: string) => void;
clusterVersion: string;
onClusterVersionChange: (version: string) => void;
}
function ClusterInformation({
clusterName,
onClusterNameChange,
clusterVersion,
onClusterVersionChange,
}: ClusterInformationProps) {
return (
<div className="flex flex-col gap-y-4">
<TextField
className="w-full"
onChange={e => onClusterNameChange(e.target.value)}
placeholder="My Awesome Server"
value={clusterName}
/>
<Dropdown placeholder="Select a version">
<Dropdown.Item>Slmalr</Dropdown.Item>
<Dropdown.Item>Slmalr 2</Dropdown.Item>
</Dropdown>
</div>
);
}

View file

@ -1,5 +1,5 @@
import Modal from '@/components/overlay/Modal';
import { Button, TextField } from '@onelauncher/common/components';
import { Button, Dropdown, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/browser/')({
@ -22,10 +22,10 @@ function RouteComponent() {
</Modal>
</Modal.Trigger>
{/* <Dropdown>
<Dropdown label="Select a version">
<Dropdown.Item>Slmalr</Dropdown.Item>
<Dropdown.Item>Slmalr 2</Dropdown.Item>
</Dropdown> */}
</Dropdown>
</div>
);
}

View file

@ -1,6 +1,7 @@
import type { Model } from '@/bindings.gen';
import DefaultBanner from '@/assets/images/default_banner.png';
import DefaultInstancePhoto from '@/assets/images/default_instance_cover.jpg';
import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation';
import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
@ -28,7 +29,7 @@ function RouteComponent() {
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center gap-x-2">
<ClusterCreate />
<NewClusterCreate />
</div>
</div>
@ -114,7 +115,7 @@ function ClusterCreate() {
const result = useCommand('createCluster', () => bindings.core.createCluster({
icon: null,
mc_loader: 'vanilla',
mc_version: '1.20.1',
mc_version: '1.8.9',
name: 'Test Cluster',
mc_loader_version: null,
}), {
@ -223,40 +224,44 @@ function ClusterCard({
};
return (
<Link
search={{
id,
}} to="/app/cluster"
>
<div
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
<>
<Link
search={{
id,
}} to="/app/cluster"
>
<div className="relative flex-1 overflow-hidden rounded-t-xl">
<div
className="absolute h-full w-full transition-transform group-hover:!scale-110"
>
<img
className="h-full w-full object-cover"
src={DefaultInstancePhoto}
/>
<div
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
>
<div className="relative flex-1 overflow-hidden rounded-t-xl">
<div
className="absolute h-full w-full transition-transform group-hover:!scale-110"
>
<img
className="h-full w-full object-cover"
src={DefaultInstancePhoto}
/>
</div>
</div>
</div>
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{name}</p>
<p className="h-4 text-xs">
{mc_loader}
{' '}
{mc_version}
{/* {' '}
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{name}</p>
<p className="h-4 text-xs">
{mc_loader}
{' '}
{mc_version}
{/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>
</div>
</p>
</div>
{/* <LaunchButton cluster={props} iconOnly /> */}
<Button onClick={handleLaunch} size="icon"><PlayIcon /></Button>
{/* <LaunchButton cluster={props} iconOnly /> */}
</div>
</div>
</div>
</Link>
</Link>
<Button onClick={handleLaunch} size="icon"><PlayIcon /></Button>
</>
);
}

View file

@ -12,7 +12,6 @@ export const Route = createFileRoute('/onboarding/login')({
});
function RouteComponent() {
const ctx = useOnboardingContext();
const [profile, setProfile] = useState<MinecraftCredentials>();
const [errorMessage, setErrorMessage] = useState<string>('');
const result = useCommand('openMsaLogin', bindings.core.openMsaLogin, {
@ -33,8 +32,6 @@ function RouteComponent() {
}
setProfile(data.data);
ctx.setIsForwardButtonEnabled(false);
});
}

View file

@ -239,7 +239,7 @@ function RouteComponent() {
]);
return (
<OnboardingContext value={ctxValue}>
<OnboardingContext.Provider value={ctxValue}>
<div className="w-full flex flex-col items-center h-screen bg-background-primary text-text-primary">
<div className="h-0.5 w-full">
<div
@ -263,7 +263,7 @@ function RouteComponent() {
</div>
</div>
</div>
</OnboardingContext>
</OnboardingContext.Provider>
);
}

View file

@ -5,5 +5,17 @@ export const Route = createFileRoute('/onboarding/summary')({
});
function RouteComponent() {
return <div>Hello "/onboarding/summary"!</div>;
return (
<div className="grid grid-cols-2 h-full w-full flex-col items-start justify-center gap-x-16 gap-y-2">
<h1 className="text-6xl">
Prepare OneLauncher
</h1>
<h3>Are you sure you want to proceed with the following tasks?</h3>
<div>
<pre>tasks</pre>
</div>
</div>
);
}

View file

@ -27,9 +27,10 @@ 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']
'style/function-paren-newline': ['error', 'consistent'],
'react/no-context-provider': 'off',
},
},
);

View file

@ -206,26 +206,57 @@ pub async fn locate_java() -> LauncherResult<HashMap<PathBuf, JavaInfo>> {
#[cfg(target_os = "windows")]
fn internal_locate_java() -> Vec<PathBuf> {
let mut found = Vec::new();
let mut java_homes = Vec::new();
// epic common paths
let common_paths = [
r"C:\Program Files\Java",
r"C:\Program Files (x86)\Java",
r"C:\Program Files\OpenJDK",
r"C:\Program Files\Eclipse Adoptium",
r"C:\Program Files\Zulu",
r"C:\Program Files\Amazon Corretto",
];
for base_path in common_paths {
if let Ok(entries) = std::fs::read_dir(base_path) {
for entry in entries.flatten() {
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
let java_exe = entry.path().join("bin").join("java.exe");
if java_exe.exists() {
java_homes.push(java_exe);
}
}
}
}
}
// env vars
if let Ok(java_home) = std::env::var("JAVA_HOME") {
let path_buf = PathBuf::from(java_home);
if path_buf.join("bin").join("java.exe").exists() {
java_homes.push(path_buf);
}
}
// TODO: Implement Registry scanning
// TODO(windows): More paths for Java installations
let paths = vec![
r"C:/Program Files/Java",
r"C:/Program Files/Eclipse Adoptium",
r"C:/Program Files (x86)/Java",
r"C:/Program Files (x86)/Eclipse Adoptium",
];
for path in paths {
let path = PathBuf::from(path).join(get_java_bin());
if path.exists() {
found.push(path);
}
}
found
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(';') {
let java_exe = PathBuf::from(path_entry).join("java.exe");
if java_exe.exists() {
if let Some(bin_dir) = java_exe.parent() {
if let Some(java_home) = bin_dir.parent() {
let java_home_path = java_home.to_path_buf();
if !java_homes.contains(&java_home_path) {
java_homes.push(java_home_path);
}
}
}
}
}
}
java_homes
}
#[cfg(target_os = "macos")]

View file

@ -1,4 +1,4 @@
use onelauncher_entity::{icon::Icon, loader::GameLoader};
use onelauncher_entity::{icon::Icon, loader::GameLoader, prelude::entity};
use tauri::{AppHandle, Runtime};
use crate::{api::{self, cluster::dao::ClusterId}, error::LauncherResult, store::{credentials::MinecraftCredentials, Core}};
@ -51,7 +51,6 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "openMsaLogin")]
async fn open_msa_login<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>>;
}
@ -207,5 +206,4 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(None)
}
}

View file

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "java_versions")]
#[serde(rename = "JavaModel")]
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key)]

View file

@ -1,27 +1,28 @@
import type {
SelectProps as AriaComboBoxProps,
ListBoxItemProps as AriaListBoxItemProps,
SelectProps as AriaSelectProps,
} from 'react-aria-components';
import type { VariantProps } from 'tailwind-variants';
import type { ClassNameString } from '../types/global';
import {
Select as AriaComboBox,
Label as AriaLabel,
ListBox as AriaListBox,
ListBoxItem as AriaListBoxItem,
Popover as AriaPopover,
Select as AriaSelect,
SelectValue,
} from 'react-aria-components';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
import { Button } from '.';
export interface ComboBoxWrapperProps<T extends object> extends Omit<AriaComboBoxProps<T>, 'className'>, ClassNameString {
export interface ComboBoxWrapperProps<T extends object> extends Omit<AriaSelectProps<T>, 'className'>, ClassNameString {
label?: string;
}
const dropdownVariants = tv({
base: [
'flex flex-col border border-component-bg-hover w-fit rounded-lg focus:outline-none',
'relative h-full',
],
});
@ -34,21 +35,18 @@ export function Dropdown<T extends object>({
...rest
}: ComboBoxWrapperProps<T>) {
return (
<AriaComboBox
className={dropdownVariants({ className })}
{...rest}
>
<AriaSelect {...rest} className={dropdownVariants({ className })}>
{label && <AriaLabel>{label}</AriaLabel>}
<div className="flex">
<Button className="border w-full border-component-bg py-1" color="ghost">
<SelectValue />
<Button className="size-6">▼</Button>
</div>
<AriaPopover className="border border-orange-400">
<AriaListBox className="bg-blue-500 p-2 w-38">
<span aria-hidden="true">▼</span>
</Button>
<AriaPopover className="border border-component-bg rounded-lg p-1">
<AriaListBox className="max-h-47 overflow-auto flex-col gap-1">
{children}
</AriaListBox>
</AriaPopover>
</AriaComboBox>
</AriaSelect>
);
}
@ -59,7 +57,7 @@ Dropdown.Item = ({
...rest
}: AriaListBoxItemProps & ClassNameString) => (
<AriaListBoxItem
className={className}
className={twMerge('hover:bg-component-bg-hover px-2 py-1 rounded-md w-full', className)}
{...rest}
/>
);