diff --git a/apps/onelauncher/desktop/capabilities/default.json b/apps/onelauncher/desktop/capabilities/default.json
index f7557ce6..b4bfbae1 100644
--- a/apps/onelauncher/desktop/capabilities/default.json
+++ b/apps/onelauncher/desktop/capabilities/default.json
@@ -35,6 +35,9 @@
{
"identifier": "opener:allow-open-path",
"allow": [
+ {
+ "path": "$DATA/OneLauncher/**"
+ },
{
"path": "$DATA/OneLauncher"
}
diff --git a/apps/onelauncher/frontend/package.json b/apps/onelauncher/frontend/package.json
index 19fd82f7..a3c44f0c 100644
--- a/apps/onelauncher/frontend/package.json
+++ b/apps/onelauncher/frontend/package.json
@@ -38,7 +38,8 @@
"tailwindcss": "^4.0.6",
"tailwindcss-animated": "^2.0.0",
"tailwindcss-react-aria-components": "^2.0.0",
- "taurpc": "^1.8.1"
+ "taurpc": "^1.8.1",
+ "valtio": "^2.1.5"
},
"devDependencies": {
"@tanstack/eslint-config": "^0.1.0",
diff --git a/apps/onelauncher/frontend/src/components/overlay/NotificationToasts.tsx b/apps/onelauncher/frontend/src/components/overlay/NotificationToasts.tsx
index e0824b22..ca4d6e16 100644
--- a/apps/onelauncher/frontend/src/components/overlay/NotificationToasts.tsx
+++ b/apps/onelauncher/frontend/src/components/overlay/NotificationToasts.tsx
@@ -17,7 +17,7 @@ interface NotificationToastProps {
const fractionEnded = (fraction: number | undefined) => fraction !== undefined && fraction >= 0.97;
-function NotificationToast({ notification, onRemove }: NotificationToastProps) {
+export function NotificationToast({ notification, onRemove }: NotificationToastProps) {
const [isVisible, setIsVisible] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
diff --git a/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx b/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx
index c1e9c9b7..eeee83c0 100644
--- a/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx
+++ b/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx
@@ -2,6 +2,7 @@ import type { NotificationData } from '@/hooks/useNotification';
import useNotifications from '@/hooks/useNotification';
import { Button, Popup } from '@onelauncher/common/components';
import { XIcon } from '@untitled-theme/icons-react';
+import { NotificationToast } from '../NotificationToasts';
interface NotificationItemProps {
id: string;
@@ -9,6 +10,7 @@ interface NotificationItemProps {
onRemove: (id: string) => void;
}
+// eslint-disable-next-line unused-imports/no-unused-vars -- deprecated
function NotificationItem({ id, data, onRemove }: NotificationItemProps) {
return (
@@ -77,11 +79,7 @@ function NotificationPopup() {
{notificationEntries.map(([id, data]) => (
))}
diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx
index f722320c..65139f4a 100644
--- a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx
+++ b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx
@@ -6,13 +6,16 @@ import ScrollableContainer from '@/components/ScrollableContainer';
import SettingsRow from '@/components/SettingsRow';
import useNotifications from '@/hooks/useNotification';
import { bindings } from '@/main';
+import { formatAsDuration } from '@/utils';
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, join } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
+import { openPath } from '@tauri-apps/plugin-opener';
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import Sidebar from '../settings/route';
@@ -37,6 +40,18 @@ function RouteComponent() {
subscribed: false,
});
+ const [launcherDir, setLauncherDir] = useState('');
+
+ useEffect(() => {
+ (async () => {
+ setLauncherDir(await join(await dataDir(), 'OneLauncher', 'clusters', cluster.data?.folder_name as string));
+ })();
+ }, [cluster.data?.folder_name]);
+
+ const openClusterDir = async () => {
+ openPath(launcherDir);
+ };
+
async function handleEditMode() {
if (edit)
await editing.refetch();
@@ -94,9 +109,10 @@ function RouteComponent() {
children="Open"
color="primary"
isDisabled={false}
+ onClick={openClusterDir}
/>
)}
- description="asdsad"
+ description={launcherDir}
disabled={false}
icon={
}
title="Cluster Folder"
@@ -207,10 +223,8 @@ function Banner({
const url = cluster?.icon_url;
- if (!url) {
- console.warn('No icon URL, using default');
+ if (!url)
return DefaultInstancePhoto;
- }
return convertFileSrc(url);
};
@@ -272,7 +286,7 @@ function Banner({
>
Played for
{' '}
-
{cluster?.overall_played || 0}
+
{formatAsDuration(cluster?.overall_played || 0)}
.
diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/logs.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/logs.tsx
index 63952f10..59e5c9f5 100644
--- a/apps/onelauncher/frontend/src/routes/app/cluster/logs.tsx
+++ b/apps/onelauncher/frontend/src/routes/app/cluster/logs.tsx
@@ -1,57 +1,52 @@
import type { ProcessPayload } from '@/bindings.gen';
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
+import { useCommand } from '@onelauncher/common';
import { Button, Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
+import { dataDir, join } from '@tauri-apps/api/path';
+import { openPath } from '@tauri-apps/plugin-opener';
import { LinkExternal01Icon, Upload01Icon } from '@untitled-theme/icons-react';
-import { useCallback, useEffect, useReducer } from 'react';
-import {
- ListBox,
- ListBoxItem,
- ListLayout,
- Virtualizer,
-} from 'react-aria-components';
+import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
+import { useEffect } from 'react';
+import { proxy, useSnapshot } from 'valtio';
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, 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;
- }
-}
+const store = proxy({
+ logs: [] as Array,
+});
function RouteComponent() {
const { id: _id } = Route.useSearch();
- const [logs, dispatch] = useReducer(reducer, []);
+ const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(_id.toString()) as unknown as bigint));
+ const state = useSnapshot(store);
- const addLog = useCallback((newLog: ProcessPayload) => {
- dispatch({ type: LoggerActionType.ADD, payload: newLog });
- }, []);
+ const addLog = (newLog: ProcessPayload) => {
+ store.logs.push(newLog);
+ };
useEffect(() => {
- bindings.events.process.on(addLog);
- }, [addLog]);
+ let cleanup: (() => void) | undefined;
+
+ bindings.events.process.on(addLog).then((e) => {
+ cleanup = e;
+ }).catch(console.error);
+
+ return () => {
+ cleanup?.();
+ };
+ }, []);
+
+ const openLogsFolder = async () => {
+ openPath(await join(await dataDir(), 'OneLauncher', 'clusters', cluster.data?.folder_name as string, 'logs'));
+ };
return (
- Overview
@@ -61,8 +56,8 @@ function RouteComponent() {
@@ -114,37 +110,114 @@ function RouteComponent() {
}
interface FormattedLogProps {
- log: Array
;
+ log: ReadonlyArray;
}
function FormattedLog(props: FormattedLogProps) {
const { log } = props;
return (
- <>
-
-
+
+
+
+
+
- {(item) => {
- if (item.type === 'Output')
- return (
-
- {item.output}
-
- );
- }}
-
-
- >
+
+ {log.map((logLine, index) => {
+ if (index === log.length - 1)
+ return null;
+
+ // trickery fuckery
+ const lineKey = `${logLine.type}-${index}-${(logLine.type === 'Output' ? logLine.output : '').slice(0, 50)}`;
+ return ;
+ })}
+
+
+
+
+ );
+}
+
+const REGEX_PATTERN = /\[(\d+:\d+:\d+)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]:? (?:\[(CHAT)\])?/;
+
+export function Line(props: { line: ProcessPayload }) {
+ const { line } = props;
+
+ const format = (logText: string) => {
+ let processedLine = logText.replace(/ยง./g, '');
+ const prefix = processedLine.match(REGEX_PATTERN);
+
+ const isEmpty = processedLine.trim() === '';
+
+ if (isEmpty)
+ processedLine = '\n';
+
+ if (prefix === null)
+ return (
+
+ {processedLine}
+
+ );
+
+ const isChatMsg = prefix[4] === 'CHAT';
+ const logLevel = prefix[3].toUpperCase();
+
+ const getLevelColor = (level: string) => {
+ switch (level) {
+ case 'INFO':
+ return 'text-code-info';
+ case 'DEBUG':
+ return 'text-code-debug';
+ case 'TRACE':
+ return 'text-code-trace';
+ case 'WARN':
+ return 'text-code-warn';
+ case 'ERROR':
+ return 'text-code-error';
+ default:
+ return 'text-fg-primary';
+ }
+ };
+
+ return (
+
+
+ {prefix[1]}
+
+
+ {`[${prefix[2]}/${prefix[3]}]`}
+
+ {isChatMsg && (
+
+ CHAT
+
+ )}
+
+ {processedLine.slice(prefix[0].length)}
+
+
+ );
+ };
+
+ return (
+ <>{format(line.type === 'Output' ? line.output : '')}>
);
}
diff --git a/apps/onelauncher/frontend/src/routes/app/index.tsx b/apps/onelauncher/frontend/src/routes/app/index.tsx
index 36637c63..ebf1efc9 100644
--- a/apps/onelauncher/frontend/src/routes/app/index.tsx
+++ b/apps/onelauncher/frontend/src/routes/app/index.tsx
@@ -4,7 +4,7 @@ import DefaultInstancePhoto from '@/assets/images/default_instance_cover.jpg';
import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation';
import useRecentCluster from '@/hooks/useCluster';
import { bindings } from '@/main';
-import { upperFirst } from '@/utils';
+import { formatAsDuration, upperFirst } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { Button, Show } from '@onelauncher/common/components';
import { createFileRoute, Link } from '@tanstack/react-router';
@@ -56,35 +56,46 @@ function Banner() {
// const launch = useLaunchCluster(() => cluster()?.uuid);
// const navigate = useNavigate();
+ const image = () => {
+ const url = cluster?.icon_url;
+
+ if (!url)
+ return DefaultBanner;
+
+ return convertFileSrc(url);
+ };
+
return (
-
![Default Banner]()
{
- (e.target as HTMLImageElement).src = DefaultBanner;
- }}
- src={cluster?.icon_url || DefaultBanner}
- />
+
+
![Default Banner]()
{
+ (e.target as HTMLImageElement).src = DefaultBanner;
+ }}
+ src={image()}
+ />
+
-
Create a cluster
+
{cluster?.name || 'Create a cluster'}
You've played
{' '}
- {cluster?.name}
+ {cluster?.mc_version}
{' '}
{upperFirst(cluster?.mc_loader || 'Unknown')}
{' '}
for
{' '}
- {cluster?.overall_played || 0}
+ {formatAsDuration(cluster?.overall_played || 0)}
.
@@ -177,9 +188,6 @@ function ClusterCard({
return convertFileSrc(url);
};
- // eslint-disable-next-line no-console -- ok
- console.log(image());
-
return (
<>
{
+ (async () => {
+ setLauncherDir(await join(await dataDir(), 'OneLauncher'));
+ })();
+ }, []);
const openLauncherDir = async () => {
- const dir = await dataDir();
-
- open(`${dir}/OneLauncher`);
+ openPath(launcherDir);
};
const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled);
@@ -60,7 +66,7 @@ function RouteComponent() {
Folders and Files
}
title="Launcher Folder"
>
diff --git a/apps/onelauncher/frontend/src/styles/global.css b/apps/onelauncher/frontend/src/styles/global.css
index 34cfceb8..06a0ee4a 100644
--- a/apps/onelauncher/frontend/src/styles/global.css
+++ b/apps/onelauncher/frontend/src/styles/global.css
@@ -13,3 +13,20 @@
--radius-3xl: 1.5rem;
--radius-4xl: 2rem;
}
+
+/* i dont know where to put this */
+.linearBlur::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ -webkit-backdrop-filter: blur(30);
+ backdrop-filter: blur(30);
+
+ -webkit-mask: linear-gradient(270deg, transparent, black 50%);
+ mask: linear-gradient(270deg, transparent, black 50%);
+
+ background: linear-gradient(270deg, transparent, rgba(0, 0, 0, 0.85) 80%);
+}
\ No newline at end of file
diff --git a/apps/onelauncher/frontend/src/utils/index.ts b/apps/onelauncher/frontend/src/utils/index.ts
index f1b1ae8c..86d99b37 100644
--- a/apps/onelauncher/frontend/src/utils/index.ts
+++ b/apps/onelauncher/frontend/src/utils/index.ts
@@ -86,3 +86,35 @@ export function formatAsRelative(
return 'now';
}
+
+function convertSeconds(secondsInput: number): string {
+ let remaining = secondsInput;
+
+ const hours = Math.floor(remaining / 3600);
+ remaining %= 3600;
+
+ const minutes = Math.floor(remaining / 60);
+ remaining %= 60;
+
+ const seconds = remaining;
+
+ const parts: Array
= [];
+
+ if (hours)
+ parts.push(`${hours} hours`);
+ if (minutes)
+ parts.push(`${minutes} minutes`);
+ if (seconds)
+ parts.push(`${seconds} seconds`);
+
+ return parts.join(' ');
+}
+
+export function formatAsDuration(seconds: number | bigint | Date): string {
+ if (seconds instanceof Date)
+ seconds = seconds.getTime() / 1000;
+ else
+ seconds = Number(seconds);
+
+ return convertSeconds(seconds);
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d1bca725..fe59b9ad 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -291,6 +291,9 @@ importers:
taurpc:
specifier: ^1.8.1
version: 1.8.1
+ valtio:
+ specifier: ^2.1.5
+ version: 2.1.5(@types/react@19.1.2)(react@19.1.0)
devDependencies:
'@tanstack/eslint-config':
specifier: ^0.1.0
@@ -4902,6 +4905,9 @@ packages:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
+ proxy-compare@3.0.1:
+ resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -5481,6 +5487,18 @@ packages:
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+ valtio@2.1.5:
+ resolution: {integrity: sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ react: '>=18.0.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+
vite-node@2.1.3:
resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -10944,6 +10962,8 @@ snapshots:
process@0.11.10: {}
+ proxy-compare@3.0.1: {}
+
punycode@2.3.1: {}
quansync@0.2.10: {}
@@ -11668,6 +11688,13 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
+ valtio@2.1.5(@types/react@19.1.2)(react@19.1.0):
+ dependencies:
+ proxy-compare: 3.0.1
+ optionalDependencies:
+ '@types/react': 19.1.2
+ react: 19.1.0
+
vite-node@2.1.3(@types/node@22.7.9)(lightningcss@1.30.1)(sass@1.80.3):
dependencies:
cac: 6.7.14