feat: cluster logs and ui tweaks

This commit is contained in:
emirsassan 2025-06-19 21:46:19 +03:00
parent fbf04cba9a
commit 1a099f6c3b
15 changed files with 340 additions and 61 deletions

60
Cargo.lock generated
View file

@ -2512,6 +2512,25 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -3482,6 +3501,7 @@ dependencies = [
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
@ -3512,6 +3532,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "open"
version = "5.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
dependencies = [
"dunce",
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -3673,6 +3705,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -6010,6 +6048,28 @@ dependencies = [
"uuid 1.16.0",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8983f50326d34437142a6d560b5c3426e91324297519b6eeb32ed0a1d1e0f2"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation 0.3.1",
"open",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"url",
"windows 0.61.1",
"zbus",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.2.3"

View file

@ -51,6 +51,7 @@ tauri-plugin-dialog = { version = "=2.2.1" }
tauri-plugin-single-instance = { version = "=2.2.3" }
tauri-plugin-updater = { version = "=2.7.1" }
tauri-plugin-window-state = { version = "=2.2.2" }
tauri-plugin-opener = { version = "=2.3.0" }
# code gen
strum = { version = "=0.27.1", features = [ "derive" ] }

View file

@ -37,6 +37,7 @@ tauri-plugin-updater = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-deep-link = { workspace = true }
tauri-plugin-opener = { workspace = true }
# code gen
taurpc = { workspace = true }

View file

@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "capabilities for the main window",
"windows": ["*"],
"windows": [
"*"
],
"permissions": [
"core:app:default",
"core:webview:default",
@ -12,7 +14,6 @@
"core:resources:default",
"core:tray:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-create",
"core:window:allow-destroy",
@ -24,13 +25,20 @@
"core:window:allow-set-decorations",
"core:window:allow-set-title-bar-style",
"core:webview:allow-internal-toggle-devtools",
"core:event:default",
"clipboard-manager:allow-write-text",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-confirm",
"deep-link:default",
"updater:default"
"updater:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "$DATA/OneLauncher"
}
]
}
]
}

View file

@ -70,6 +70,7 @@ async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_opener::init())
.menu(tauri::menu::Menu::new)
.invoke_handler(router.into_handler())
.setup(move |app| {

View file

@ -24,6 +24,7 @@
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.2.1",
"@untitled-theme/icons-react": "^0.14.0",
"motion": "^12.10.5",

View file

@ -1,4 +1,4 @@
import type { IngressPayload, Version, VersionType } from '@/bindings.gen';
import type { GameLoader, IngressPayload, Version, VersionType } from '@/bindings.gen';
import type { JSX, RefAttributes } from 'react';
import DefaultBanner from '@/assets/images/default_banner.png';
import DefaultClusterBanner from '@/assets/images/default_instance_cover.jpg';
@ -6,7 +6,7 @@ import LauncherIcon from '@/components/content/LauncherIcon';
import Modal from '@/components/overlay/Modal';
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
import { LAUNCHER_IMPORT_TYPES } from '@/utils';
import { LAUNCHER_IMPORT_TYPES, LOADERS, upperFirst } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { Button, Dropdown, SelectList, TextField } from '@onelauncher/common/components';
import { useQueryClient } from '@tanstack/react-query';
@ -36,7 +36,7 @@ export function NewClusterCreate() {
const create = useCommand('createCluster', () => bindings.core.createCluster({
icon: DefaultClusterBanner,
mc_loader: 'vanilla',
mc_loader: formData.loader as GameLoader,
mc_loader_version: null,
mc_version: formData.clusterVersion,
name: formData.clusterName,
@ -91,6 +91,7 @@ export function NewClusterCreate() {
<ClusterInformation
clusterName={formData.clusterName}
clusterVersion={formData.clusterVersion}
onClusterLoaderChange={loader => setFormData(prev => ({ ...prev, loader }))}
onClusterNameChange={name => setFormData(prev => ({ ...prev, clusterName: name }))}
onClusterVersionChange={version =>
setFormData(prev => ({ ...prev, clusterVersion: version }))}
@ -231,6 +232,7 @@ interface ClusterInformationProps {
onClusterNameChange: (name: string) => void;
clusterVersion: string;
onClusterVersionChange: (version: string) => void;
onClusterLoaderChange: (loader: GameLoader) => void;
}
function ClusterInformation({
@ -238,6 +240,7 @@ function ClusterInformation({
onClusterNameChange,
clusterVersion,
onClusterVersionChange,
onClusterLoaderChange,
}: ClusterInformationProps) {
const versions = useCommand('getGameVersions', bindings.core.getGameVersions);
@ -258,35 +261,17 @@ function ClusterInformation({
</Option>
<Option header="Loader">
<Dropdown defaultSelectedKey="vanilla">
<Dropdown.Item>
<div className="flex flex-row">
<LoaderIcon className="size-5" loader="vanilla" />
{' '}
Vanilla
</div>
</Dropdown.Item>
<Dropdown.Item>
<div className="flex flex-row">
<LoaderIcon className="size-5" loader="fabric" />
{' '}
Fabric
</div>
</Dropdown.Item>
<Dropdown.Item>
<div className="flex flex-row">
<LoaderIcon className="size-5" loader="quilt" />
{' '}
Quilt
</div>
</Dropdown.Item>
<Dropdown.Item>
<div className="flex flex-row">
<LoaderIcon className="size-5" loader="forge" />
{' '}
Forge
</div>
</Dropdown.Item>
{/* TODO: fixme */}
<Dropdown defaultSelectedKey="vanilla" onSelectionChange={e => onClusterLoaderChange(e)}>
{LOADERS.map(loader => (
<Dropdown.Item key={loader}>
<div className="flex flex-row">
<LoaderIcon className="size-5" loader={loader as GameLoader} />
{' '}
{upperFirst(loader)}
</div>
</Dropdown.Item>
))}
</Dropdown>
</Option>
</div>

View file

@ -8,7 +8,9 @@ import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Show, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { convertFileSrc } from '@tauri-apps/api/core';
import { dataDir } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
@ -20,12 +22,12 @@ export const Route = createFileRoute('/app/cluster/')({
function RouteComponent() {
const { id } = Route.useSearch();
const [newCover, setNewCover] = useState<string>('');
const [newName, setNewName] = useState<string>('');
const [edit, setEdit] = useState<boolean>(false);
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
// dumbass fix ik
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
const [newCover, setNewCover] = useState<string>(cluster.data?.icon_url as string);
const [newName, setNewName] = useState<string>(cluster.data?.name as string);
const [edit, setEdit] = useState<boolean>(false);
const editing = useCommand('updateClusterById', () => bindings.core.updateClusterById(Number(id.toString()) as unknown as bigint, {
icon_url: newCover,
@ -35,6 +37,11 @@ function RouteComponent() {
subscribed: false,
});
bindings.events.process.on((e) => {
// eslint-disable-next-line no-console -- ok
console.log(e);
});
function handleEditMode() {
if (edit)
editing.refetch();
@ -126,22 +133,24 @@ function Banner({
cluster,
editMode,
setNewName,
setNewCover,
}: BannerProps) {
// async function launchFilePicker() {
// const selected = await dialog.open({
// multiple: false,
// directory: false,
// filters: [{
// name: 'Image',
// extensions: ['png', 'jpg', 'jpeg', 'webp'],
// }],
// });
async function launchFilePicker() {
const selected = await open({
multiple: false,
directory: false,
filters: [{
name: 'Image',
extensions: ['png', 'jpg', 'jpeg', 'webp'],
}],
});
// if (selected === null)
// return;
if (selected === null)
return;
setNewCover(selected);
}
// props.setNewCover(selected);
// }
const launch = useCommand('launchCluster', () => bindings.core.launchCluster(cluster?.id as bigint, null), {
enabled: false,
subscribed: false,
@ -161,20 +170,29 @@ function Banner({
setNewName(name);
}
const image = () => {
const url = cluster?.icon_url;
if (url === undefined || url === null)
return DefaultInstancePhoto;
return convertFileSrc(url);
};
return (
<div className="h-37 flex flex-row gap-x-2.5 rounded-xl bg-page-elevated p-2.5">
<div className="relative aspect-ratio-video h-full min-w-57 w-57 overflow-hidden border border-component-bg/10 rounded-lg">
<Show when={editMode}>
<div
className="absolute z-1 h-full w-full flex items-center justify-center bg-black/50 opacity-50 hover:opacity-100"
// onClick={launchFilePicker}
onClick={launchFilePicker}
>
<ImagePlusIcon className="h-12 w-12" />
</div>
</Show>
{/* <ClusterCover class="h-full w-full object-cover" cluster={props.cluster} override={props.newCover()} /> */}
<img src={cluster?.icon_url || DefaultInstancePhoto} />
<img src={image()} />
</div>
<div className="w-full flex flex-col justify-between gap-y-.5 overflow-hidden text-fg-primary">
@ -222,8 +240,7 @@ function Banner({
<Button
children={<Share07Icon />}
color="secondary"
// disabled={props.editMode()}
isDisabled
isDisabled={editMode}
/>
</div>
</div>

View file

@ -0,0 +1,150 @@
import type { ProcessPayload } from '@/bindings.gen';
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
import { Button, Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { LinkExternal01Icon, Upload01Icon } from '@untitled-theme/icons-react';
import { useCallback, useEffect, useReducer } from 'react';
import {
ListBox,
ListBoxItem,
ListLayout,
Virtualizer,
} from 'react-aria-components';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/logs')({
component: RouteComponent,
});
enum LoggerActionType {
ADD,
}
interface LoggerAction {
type: LoggerActionType;
payload: ProcessPayload;
}
function reducer(state: Array<ProcessPayload>, action: LoggerAction) {
const { type, payload } = action;
switch (type) {
// eslint-disable-next-line ts/no-unnecessary-condition -- oh please shut the fuck up i dont want to listen your yapping anymore
case LoggerActionType.ADD:
return [...state, payload];
default:
return state;
}
}
function RouteComponent() {
const { id: _id } = Route.useSearch();
const [logs, dispatch] = useReducer(reducer, []);
const addLog = useCallback((newLog: ProcessPayload) => {
dispatch({ type: LoggerActionType.ADD, payload: newLog });
}, []);
useEffect(() => {
bindings.events.process.on(addLog);
}, [addLog]);
return (
<Sidebar.Page>
<h1>Overview</h1>
<ScrollableContainer>
<div className="h-full">
<div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-col gap-y-2">
<div className="h-10 flex flex-row items-center justify-between gap-x-1">
<h1>Logs</h1>
<div className="flex flex-row gap-x-2">
<Button
color="secondary"
// isDisabled={missingLogs()}
// onClick={uploadAndOpenLog}
>
<Upload01Icon />
{' '}
Upload
</Button>
{/* <Dropdown
class="min-w-50"
disabled={missingLogs()}
onChange={changeLog}
>
<For each={logs() || ['None']}>
{(log) => {
const pretty = log.split('/').pop();
return (
<Dropdown.Row>
<div>
{pretty}
</div>
</Dropdown.Row>
);
}}
</For>
</Dropdown> */}
<Button
color="primary"
// onClick={openFolder}
>
<LinkExternal01Icon />
{' '}
Open Folder
</Button>
</div>
</div>
<Show
fallback={<span>No logs were found.</span>}
when={logs.length > 0}
>
<FormattedLog log={logs} />
</Show>
</div>
</div>
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}
interface FormattedLogProps {
log: Array<ProcessPayload>;
}
function FormattedLog(props: FormattedLogProps) {
const { log } = props;
return (
<>
<Virtualizer
layout={ListLayout}
layoutOptions={{
rowHeight: 32,
padding: 4,
gap: 4,
}}
>
<ListBox
aria-label="Virtualized ListBox"
items={log}
selectionMode="none"
>
{(item) => {
if (item.type === 'Output')
return (
<ListBoxItem key={item.output}>
{item.output}
</ListBoxItem>
);
}}
</ListBox>
</Virtualizer>
</>
);
}

View file

@ -1,4 +1,4 @@
import type { ClusterModel } from '@/bindings.gen';
import type { ClusterModel, IngressType } 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';
@ -7,7 +7,8 @@ import { bindings } from '@/main';
import { upperFirst } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { Button, Show, TextField } from '@onelauncher/common/components';
import { createFileRoute, Link } from '@tanstack/react-router';
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
import { convertFileSrc } from '@tauri-apps/api/core';
import { PlayIcon, Server01Icon } from '@untitled-theme/icons-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
@ -26,6 +27,13 @@ hey future sassan here i guess the issue is solved idk
function RouteComponent() {
const result = useCommand('getClusters', bindings.core.getClusters);
bindings.events.ingress.on((e) => {
if (typeof e.ingress_type === 'object')
if ('PrepareCluster' in e.ingress_type)
// eslint-disable-next-line no-console -- ok
console.log(`Preparing cluster: ${e.ingress_type.PrepareCluster.cluster_name} %${e.percent}`);
});
return (
<div className="h-full flex flex-col gap-y-4 text-fg-primary">
<Banner />
@ -149,6 +157,7 @@ function ClusterCard({
name,
mc_loader,
mc_version,
icon_url,
}: ClusterModel) {
const launch = useCommand('launchCluster', () => bindings.core.launchCluster(id, null), {
enabled: false,
@ -162,6 +171,15 @@ function ClusterCard({
console.error(launch.error.message);
};
const image = () => {
const url = icon_url;
if (url === null)
return DefaultInstancePhoto;
return convertFileSrc(url);
};
return (
<>
<Link
@ -178,7 +196,7 @@ function ClusterCard({
>
<img
className="h-full w-full object-cover"
src={DefaultInstancePhoto}
src={image()}
/>
</div>
</div>

View file

@ -4,6 +4,8 @@ import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Switch } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { dataDir } from '@tauri-apps/api/path';
import { openPath as open } from '@tauri-apps/plugin-opener';
import { FolderIcon, LinkExternal01Icon, XIcon } from '@untitled-theme/icons-react';
import Sidebar from './route';
@ -14,6 +16,12 @@ export const Route = createFileRoute('/app/settings/')({
function RouteComponent() {
const _result = useCommand('getGlobalProfile', bindings.core.getGlobalProfile);
const openLauncherDir = async () => {
const dir = await dataDir();
open(`${dir}/OneLauncher`);
};
return (
<Sidebar.Page>
<div className="h-full">
@ -52,7 +60,7 @@ function RouteComponent() {
icon={<FolderIcon />}
title="Launcher Folder"
>
<Button size="normal">
<Button onClick={openLauncherDir} size="normal">
<LinkExternal01Icon />
{' '}
Open

View file

@ -10,6 +10,15 @@ export const LAUNCHER_IMPORT_TYPES: Array<string> = [
// 'Technic'
] as const;
export const LOADERS: Array<string> = [
'vanilla',
'forge',
'neoforge',
'quilt',
'fabric',
'legacyfabric',
] as const;
export function pluralize(n: number, word: string, locale: string = 'en'): string {
const pluralRules = new Intl.PluralRules(locale);
const pluralForm = pluralRules.select(n);

View file

@ -105,6 +105,15 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
async fn create_cluster(self, options: CreateCluster) -> LauncherResult<Cluster> {
let cluster = api::cluster::create_cluster(&options.name, &options.mc_version, options.mc_loader, options.mc_loader_version.as_deref(), options.icon).await?;
api::setting_profiles::create_profile(&options.name, |mut active_model: SettingsProfilePartial| async move {
active_model.force_fullscreen = Set(Some(false));
active_model.mem_max = Set(Some(2048));
Ok(active_model)
})
.await?;
Ok(cluster)
}

View file

@ -36,6 +36,7 @@ const dropdownStyles = tv({
triggerCustomIcon: 'w-4 h-4',
popover: [
// width is not full instead it behaves like w-screen for some fucking reason
'mt-1 rounded-lg shadow-md',
'bg-component-bg border border-component-border',
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:zoom-in-95',

10
pnpm-lock.yaml generated
View file

@ -246,6 +246,9 @@ importers:
'@tauri-apps/plugin-dialog':
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-opener':
specifier: ^2.3.0
version: 2.3.0
'@tauri-apps/plugin-shell':
specifier: ^2.2.1
version: 2.2.1
@ -2811,6 +2814,9 @@ packages:
'@tauri-apps/plugin-dialog@2.2.1':
resolution: {integrity: sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==}
'@tauri-apps/plugin-opener@2.3.0':
resolution: {integrity: sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw==}
'@tauri-apps/plugin-shell@2.2.1':
resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==}
@ -8504,6 +8510,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-opener@2.3.0':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-shell@2.2.1':
dependencies:
'@tauri-apps/api': 2.5.0