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