From 6d4e0fa850476dc2532711f1fc2c1006edca494a Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:14:56 +0200 Subject: [PATCH 1/3] feat: log viewer --- Cargo.lock | 32 +++++ Cargo.toml | 5 +- apps/oneclient/desktop/src/lib.rs | 7 +- apps/oneclient/frontend/package.json | 2 +- apps/oneclient/frontend/src/bindings.gen.ts | 52 +++++---- .../src/components/LogViewer.module.css | 56 +++++++++ .../frontend/src/components/LogViewer.tsx | 110 ++++++++++++++++++ .../frontend/src/components/SheetPage.tsx | 39 +++++-- .../frontend/src/components/index.ts | 2 + .../frontend/src/routes/app/cluster/logs.tsx | 69 +++++++---- .../frontend/src/routes/app/cluster/route.tsx | 71 +++++++---- packages/core/Cargo.toml | 1 + packages/core/src/api/cluster/content.rs | 56 +++++++++ packages/core/src/api/packages/dao.rs | 2 - packages/core/src/api/tauri/commands.rs | 36 +++++- packages/core/src/api/tauri/folders.rs | 28 +++++ packages/core/src/api/tauri/mod.rs | 15 +++ packages/core/src/error.rs | 6 + .../web_common/src/components/Dropdown.tsx | 11 +- packages/web_common/src/hooks/useCommand.ts | 19 +-- pnpm-lock.yaml | 26 ++++- pnpm-workspace.yaml | 5 +- 22 files changed, 541 insertions(+), 109 deletions(-) create mode 100644 apps/oneclient/frontend/src/components/LogViewer.module.css create mode 100644 apps/oneclient/frontend/src/components/LogViewer.tsx create mode 100644 packages/core/src/api/tauri/folders.rs diff --git a/Cargo.lock b/Cargo.lock index 1a4d9b3a..195ec597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,6 +626,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata 0.4.9", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -3022,6 +3033,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -3429,6 +3449,7 @@ dependencies = [ "onelauncher_gamemode", "onelauncher_macro", "onelauncher_migration", + "opener", "p256", "paste", "rand 0.8.5", @@ -3545,6 +3566,17 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "opener" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223" +dependencies = [ + "bstr", + "normpath", + "windows-sys 0.59.0", +] + [[package]] name = "option-ext" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 66590318..81dc2a18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,10 +99,6 @@ uuid = { version = "1.16", features = [ ] } # network -# async-tungstenite = { version = "0.28", features = [ -# "tokio-runtime", -# "tokio-rustls-webpki-roots" -# ] } reqwest = { version = "0.12", default-features = false, features = [ "json", "stream", @@ -174,6 +170,7 @@ merge = { version = "=0.2.0" } sysinfo = { version = "=0.34.2", features = [ "serde" ] } thiserror = { version = "2" } whoami = { version = "=1.6.0" } +opener = { version = "=0.8.2" } # macro paste = { version = "1" } diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs index bab5afdc..2bc41045 100644 --- a/apps/oneclient/desktop/src/lib.rs +++ b/apps/oneclient/desktop/src/lib.rs @@ -1,5 +1,5 @@ use onelauncher_core::api::proxy::ProxyTauri; -use onelauncher_core::api::tauri::{TauriLauncherApi, TauriLauncherEventApi}; +use onelauncher_core::api::tauri::TauRPCLauncherExt; use onelauncher_core::error::LauncherResult; use onelauncher_core::store::proxy::ProxyState; use onelauncher_core::store::semaphore::SemaphoreStore; @@ -56,12 +56,11 @@ async fn initialize_tauri(builder: tauri::Builder) -> LauncherResult let router = taurpc::Router::new() .export_config( specta_typescript::Typescript::default() - .bigint(specta_typescript::BigIntExportBehavior::BigInt) + .bigint(specta_typescript::BigIntExportBehavior::Number) .formatter(ext::specta::formatter), ) .merge(api::commands::OneClientApiImpl.into_handler()) - .merge(onelauncher_core::api::tauri::TauriLauncherApiImpl.into_handler()) - .merge(onelauncher_core::api::tauri::TauriLauncherEventApiImpl.into_handler()); + .use_launcher_api(); let builder = builder .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index a32c6add..f46e29b7 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -21,11 +21,11 @@ "@tanstack/react-router": "catalog:", "@tanstack/react-router-devtools": "catalog:", "@tanstack/react-table": "catalog:", + "@tanstack/react-virtual": "catalog:", "@tanstack/router-plugin": "catalog:", "@tauri-apps/api": "catalog:", "@tauri-apps/plugin-clipboard-manager": "catalog:", "@tauri-apps/plugin-dialog": "catalog:", - "@tauri-apps/plugin-shell": "catalog:", "@untitled-theme/icons-react": "catalog:", "motion": "catalog:", "overlayscrollbars": "catalog:", diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 9a5dfc93..a5d1b1ab 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -6,7 +6,7 @@ type TAURI_CHANNEL = (response: T) => void export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MismatchedVersion"; data: string } | { type: "MismatchedLoader"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string } -export type ClusterModel = { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_modpack_hash: string | null } +export type ClusterModel = { id: number; folder_name: string; stage: ClusterStage; created_at: string; group_id: number | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: number | null; icon_url: Icon | null; setting_profile_name: string | null; linked_modpack_hash: string | null } export type ClusterStage = "notready" | "downloading" | "repairing" | "ready" @@ -42,24 +42,24 @@ export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string } -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"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "TauriError"; data: string } +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"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "OpenerError"; data: string } | { type: "TauriError"; data: string } export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: ManagedPackageBody; /** * Won't have all versions for some providers, like CurseForge. * Try making a request to get the versions if needed */ -version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: PackageCategories; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: bigint; gallery: PackageGallery[] } +version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: PackageCategories; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: number; gallery: PackageGallery[] } export type ManagedPackageBody = { Url: string } | { Raw: string } export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null } -export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: bigint; files: ManagedVersionFile[] } +export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: number; files: ManagedVersionFile[] } export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType } -export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: bigint } +export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: number } export type MessageLevel = "Info" | "Warn" | "Error" @@ -139,9 +139,9 @@ export type PackageStatus = "active" | "abandoned" export type PackageType = "mod" | "resourcepack" | "shader" | "datapack" | "modpack" -export type Paginated = { total: bigint; offset: bigint; limit: bigint; items: T[] } +export type Paginated = { total: number; offset: number; limit: number; items: T[] } -export type Process = { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string } +export type Process = { pid: number; started_at: string; cluster_id: number; post_hook: string | null; account_id: string } export type ProcessError = { type: "HookUnsuccessful"; data: string } | { type: "NoPid"; data: string } @@ -153,19 +153,19 @@ export type Provider = "Modrinth" | "CurseForge" | "SkyClient" export type Resolution = { width: number; height: number } -export type SearchQuery = { query: string | null; offset: bigint | null; limit: bigint | null; sort: Sort | null; filters: Filters | null } +export type SearchQuery = { query: string | null; offset: number | null; limit: number | null; sort: Sort | null; filters: Filters | null } -export type SearchResult = { project_id: string; package_type: PackageType; slug: string; author: string; title: string; description: string; categories: PackageCategories; loaders: GameLoader[]; mc_versions: string[]; downloads: bigint; icon_url: string; date_created: string; date_modified: string; latest_version: string; license: string | null; client_side: PackageSide; server_side: PackageSide; +export type SearchResult = { project_id: string; package_type: PackageType; slug: string; author: string; title: string; description: string; categories: PackageCategories; loaders: GameLoader[]; mc_versions: string[]; downloads: number; icon_url: string; date_created: string; date_modified: string; latest_version: string; license: string | null; client_side: PackageSide; server_side: PackageSide; /** * List of URLs to images */ gallery: string[] } -export type SettingProfileModel = { name: string; java_id: bigint | null; res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null; os_extra: SettingsOsExtra | null } +export type SettingProfileModel = { name: string; java_id: number | null; res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null; os_extra: SettingsOsExtra | null } -export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: bigint; settings_version: number; native_window_frame: boolean } +export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean } -export type SettingsOsExtra = Record +export type SettingsOsExtra = { enable_gamemode: boolean | null } export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated" @@ -228,15 +228,17 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"getClusters":[],"updateClusterById":["id","request"],"getGameVersions":[],"searchPackages":["provider","query"],"getUser":["uuid"],"getWorlds":["id"],"readSettings":[],"getPackage":["provider","slug"],"getClusterById":["id"],"writeSettings":["setting"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"getUsersFromAuthor":["provider","author"],"getGlobalProfile":[],"getUsers":[],"removeUser":["uuid"],"removeCluster":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"openMsaLogin":[],"setDefaultUser":["uuid"],"getDefaultUser":["fallback"],"getScreenshots":["id"]}' } +const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"getScreenshots":["id"],"updateClusterById":["id","request"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"launchCluster":["id","uuid"],"getClusters":[],"getLoadersForVersion":["mc_version"],"getUser":["uuid"],"getGameVersions":[],"getDefaultUser":["fallback"],"getUsersFromAuthor":["provider","author"],"getProfileOrDefault":["name"],"removeCluster":["id"],"getUsers":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"getWorlds":["id"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"openMsaLogin":[],"getPackage":["provider","slug"],"writeSettings":["setting"],"createCluster":["options"],"readSettings":[],"getLogByName":["id","name"],"updateClusterProfile":["name","profile"],"getClusterById":["id"],"searchPackages":["provider","query"],"getGlobalProfile":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' } export type Router = { 'core': { getClusters: () => Promise, -getClusterById: (id: bigint) => Promise, -removeCluster: (id: bigint) => Promise, +getClusterById: (id: number) => Promise, +removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, -launchCluster: (id: bigint, uuid: string | null) => Promise, -updateClusterById: (id: bigint, request: ClusterUpdate) => Promise, -getScreenshots: (id: bigint) => Promise, -getWorlds: (id: bigint) => Promise, +launchCluster: (id: number, uuid: string | null) => Promise, +updateClusterById: (id: number, request: ClusterUpdate) => Promise, +getScreenshots: (id: number) => Promise, +getWorlds: (id: number) => Promise, +getLogs: (id: number) => Promise, +getLogByName: (id: number, name: string) => Promise, getProfileOrDefault: (name: string | null) => Promise, getGlobalProfile: () => Promise, updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise, @@ -254,14 +256,16 @@ searchPackages: (provider: Provider, query: SearchQuery) => Promise Promise, getPackageBody: (provider: Provider, body: ManagedPackageBody) => Promise, getMultiplePackages: (provider: Provider, slugs: string[]) => Promise, -getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: bigint, limit: bigint) => Promise>, -downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: bigint, skipCompatibility: boolean | null) => Promise, +getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise>, +downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise, getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise }, -'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise> }, 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise } }; +process: (event: ProcessPayload) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise> }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/LogViewer.module.css b/apps/oneclient/frontend/src/components/LogViewer.module.css new file mode 100644 index 00000000..53312b83 --- /dev/null +++ b/apps/oneclient/frontend/src/components/LogViewer.module.css @@ -0,0 +1,56 @@ +@reference "../styles/global.css"; + +.log_container { + @apply w-full whitespace-pre text-base font-mono bg-component-bg rounded-xl; +} + +.log { + @apply relative w-full whitespace-pre text-base font-mono; +} + +.log * { + white-space: pre !important; + user-select: text !important; + -webkit-user-select: text !important; +} + +.log > span { + @apply box-content left-0 top-0 absolute h-full w-full rounded-md hover:bg-component-bg-hover; +} + +.log > span[data-level=INFO] { + @apply text-code-info; +} + +.log > span[data-level=WARN] { + @apply text-code-warn; +} + +.log > span[data-level=ERROR] { + @apply text-code-error; +} + +.log > span[data-level=DEBUG] { + @apply text-code-debug; +} + +.log > span[data-level=TRACE] { + @apply text-code-trace; +} + +.log > span > span:nth-child(1) { + @apply relative text-transparent; +} + +.log > span > span:nth-child(1):before { + content: attr(data-time); + @apply absolute h-full w-full text-center select-text text-fg-secondary opacity-100; +} + +.log > span > span:nth-child(2) { + @apply font-bold; +} + +.log > span > span[data-chat="true"] { + @apply text-white; +} diff --git a/apps/oneclient/frontend/src/components/LogViewer.tsx b/apps/oneclient/frontend/src/components/LogViewer.tsx new file mode 100644 index 00000000..7821417e --- /dev/null +++ b/apps/oneclient/frontend/src/components/LogViewer.tsx @@ -0,0 +1,110 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { useImperativeHandle, useRef, useState } from 'react'; +import styles from './LogViewer.module.css'; + +export interface LogViewerProps { + content: string; + scrollRef: React.RefObject; + ref?: React.RefObject; +} + +export interface LogViewerRef { + push: (line: string) => void; +} + +const LINE_HEIGHT = 16; +const REGEX_PATTERN = /\[((?:\S+ )?\d+:\d+:\d+(?:\.\d+)?)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]:? (?:\[(CHAT)\])?/; + +export function LogViewer({ + content, + scrollRef, + ref, +}: LogViewerProps) { + const linesRef = useRef>(content.split('\n')); + + // used to force re-render (and re-init of virtualizer hook) + const [lastUpdated, setLastUpdated] = useState(Date.now()); + + const virtualizer = useVirtualizer({ + count: linesRef.current.length, + estimateSize: () => LINE_HEIGHT, + getScrollElement: () => scrollRef.current ?? null, + paddingStart: 4, + paddingEnd: 4, + gap: 0, + overscan: 20, + }); + + useImperativeHandle(ref, () => ({ + push: (line: string) => { + linesRef.current.push(line); + setLastUpdated(Date.now()); + }, + }), []); + + return ( +
+ + + + {virtualizer.getVirtualItems().map(virtualRow => ( + + ))} + + +
+ ); +} + +function LogLine({ + line, + style, +}: { + line: string; + style: React.CSSProperties; +}) { + line = line.replace(/ยง./g, ''); // remove color codes + const groups = line.match(REGEX_PATTERN); + + if (groups === null) + return ( + + {line} +
+
+ ); + + const [matched, time, threadName, level, isChat] = groups as [string, string, string, string, string | undefined]; + + return ( + + [{time}] + [{threadName}/{level}] + {isChat ? ' [CHAT] ' : ' '}{line.slice(matched.length)} +
+
+ ); +} diff --git a/apps/oneclient/frontend/src/components/SheetPage.tsx b/apps/oneclient/frontend/src/components/SheetPage.tsx index 0c09f5a5..0480121a 100644 --- a/apps/oneclient/frontend/src/components/SheetPage.tsx +++ b/apps/oneclient/frontend/src/components/SheetPage.tsx @@ -4,20 +4,28 @@ import { useCanGoBack, useRouter } from '@tanstack/react-router'; import { ArrowLeftIcon } from '@untitled-theme/icons-react'; import { motion, useScroll, useTransform } from 'motion/react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { twMerge } from 'tailwind-merge'; +export interface SheetPageProps { + headerSmall: React.ReactNode; + headerLarge: React.ReactNode; + children?: React.ReactNode; + scrollContainerRef?: React.RefObject; +} + +export interface SheetPageContextApi { + scrollContainerRef: React.RefObject; +} + +const SheetPageContext = React.createContext(null); + export function SheetPage({ headerSmall, headerLarge, children, scrollContainerRef, -}: { - headerSmall: React.ReactNode; - headerLarge: React.ReactNode; - children?: React.ReactNode; - scrollContainerRef?: React.RefObject; -}) { +}: SheetPageProps) { const scrollContainer = useRef(null); const headerLargeRef = useRef(null); @@ -39,6 +47,10 @@ export function SheetPage({ const smallOpacity = useTransform(scrollYProgress, [0, 1], ['1', '0']); const largeOpacity = useTransform(scrollYProgress, [0, 1], ['0', '1']); + const context = useMemo(() => ({ + scrollContainerRef: scrollContainer, + }), [scrollContainer]); + return (
@@ -63,7 +75,9 @@ export function SheetPage({ {headerLarge} - {children} + + {children} +
@@ -71,6 +85,15 @@ export function SheetPage({ ); } +export function useSheetPageContext() { + const ctx = React.useContext(SheetPageContext); + + if (!ctx) + throw new Error('useSheetPageContext must be used within a SheetPageContext.Provider'); + + return ctx; +} + SheetPage.Content = ({ children, className, diff --git a/apps/oneclient/frontend/src/components/index.ts b/apps/oneclient/frontend/src/components/index.ts index 97466938..a7c7f430 100644 --- a/apps/oneclient/frontend/src/components/index.ts +++ b/apps/oneclient/frontend/src/components/index.ts @@ -1,5 +1,7 @@ export * from './GameBackground'; export * from './Loader'; +export * from './LogViewer'; export * from './Navbar'; +export * from './SheetPage'; export * from './Spinner'; export * from './Tabs'; diff --git a/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx b/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx index 9e36bf78..8a680b6f 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx @@ -1,41 +1,66 @@ -import { SheetPage } from '@/components/SheetPage'; +import type { Key } from 'react-aria-components'; +import { SheetPage, useSheetPageContext } from '@/components'; +import { LogViewer } from '@/components/LogViewer'; import { bindings } from '@/main'; +import { useCommand, useCommandSuspense } from '@onelauncher/common'; +import { Dropdown } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { File02Icon, File05Icon } from '@untitled-theme/icons-react'; +import { useEffect, useState } from 'react'; export const Route = createFileRoute('/app/cluster/logs')({ component: RouteComponent, - async beforeLoad(ctx) { - const cluster = await bindings.core.getClusterById(ctx.search.clusterId as unknown as bigint); - // TODO: make it use actual data once that's available - const logFile = { - fileName: 'latest.log', - content: await (await fetch('https://api.mclo.gs/1/raw/M1eHlVd')).text(), - }; - return { cluster, logFile }; - }, }); function RouteComponent() { - const { logFile } = Route.useRouteContext(); + const { cluster } = Route.useRouteContext(); + const { scrollContainerRef } = useSheetPageContext(); + + const { data: fileNames } = useCommandSuspense(['getLogs', cluster.id.toString()], () => bindings.core.getLogs(cluster.id)); + + const [activeFileName, setActiveFileName] = useState(fileNames[0] || undefined); return (
-
-

Logs

- - - {logFile.fileName} - +

Logs

+ +
+ setActiveFileName(e)} selectedKey={activeFileName}> + {fileNames.map(fileName => ( + + {fileName} + + ))} +
- - {logFile.content} - - {/* Additional content can be added here */} + + {activeFileName && ()}
); } + +function LogContent({ + fileName, + scrollRef, +}: { + fileName: string; + scrollRef: React.RefObject; +}) { + const { cluster } = Route.useRouteContext(); + + // TODO: Fix cache + const { data: content } = useCommand( + ['getLogByName', cluster.id.toString(), fileName], + () => bindings.core.getLogByName(cluster.id, fileName), + { + gcTime: 1000 * 60 * 1, // 1 minute + }, + ); + + return ( + content && + ); +} diff --git a/apps/oneclient/frontend/src/routes/app/cluster/route.tsx b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx index 96f22a36..26e82c79 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/route.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx @@ -1,10 +1,11 @@ -import { Tab, TabList } from '@/components'; +import { LoaderSuspense, Tab, TabList } from '@/components'; import { SheetPage } from '@/components/SheetPage'; import { bindings } from '@/main'; import { prettifyLoader } from '@/utils/loaders'; import { getVersionInfoOrDefault } from '@/utils/versionMap'; import { Button } from '@onelauncher/common/components'; import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; +import { FolderIcon } from '@untitled-theme/icons-react'; import { useMotionValueEvent, useScroll } from 'motion/react'; import { useRef, useState } from 'react'; @@ -27,7 +28,7 @@ export const Route = createFileRoute('/app/cluster')({ const query = context.queryClient.ensureQueryData({ queryKey: ['getClusterById', search.clusterId], - queryFn: () => bindings.core.getClusterById(search.clusterId as unknown as bigint), + queryFn: () => bindings.core.getClusterById(search.clusterId), }); const cluster = await query; @@ -71,7 +72,9 @@ function RouteComponent() {
- + + +
); @@ -81,13 +84,13 @@ function HeaderLarge() { const { cluster } = Route.useRouteContext(); const versionInfo = getVersionInfoOrDefault(cluster.mc_version); - const launch = () => { - bindings.core.launchCluster(cluster.id as unknown as bigint, null); - }; + const openFolder = () => bindings.folders.openCluster(cluster.folder_name); + + const launch = () => bindings.core.launchCluster(cluster.id, null); return ( -
-
+
+
{versionInfo.tags.map(tag => ( @@ -99,13 +102,23 @@ function HeaderLarge() {

{versionInfo.longDescription}

- +
+ + + +
); } @@ -113,20 +126,30 @@ function HeaderLarge() { function HeaderSmall() { const { cluster } = Route.useRouteContext(); - const launch = () => { - bindings.core.launchCluster(cluster.id as unknown as bigint, null); - }; + const openFolder = () => bindings.folders.openCluster(cluster.folder_name); + + const launch = () => bindings.core.launchCluster(cluster.id, null); return (

{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}

- +
+ + + +
); } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index ba162198..50c3fbbc 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -73,6 +73,7 @@ async-stream = { workspace = true } paste = { workspace = true } byteorder = { workspace = true } merge = { workspace = true } +opener = { workspace = true } # integration discord-rich-presence = { workspace = true } diff --git a/packages/core/src/api/cluster/content.rs b/packages/core/src/api/cluster/content.rs index 97411227..daba14c8 100644 --- a/packages/core/src/api/cluster/content.rs +++ b/packages/core/src/api/cluster/content.rs @@ -4,6 +4,7 @@ use crate::error::LauncherResult; use crate::store::Dirs; use crate::utils::io; +/// Returns a list of screenshot file names pub async fn get_screenshots(cluster: &clusters::Model) -> LauncherResult> { let dir = cluster.folder_name.clone(); let path = Dirs::get_clusters_dir() @@ -25,6 +26,7 @@ pub async fn get_screenshots(cluster: &clusters::Model) -> LauncherResult LauncherResult> { let dir = cluster.folder_name.clone(); let path = Dirs::get_clusters_dir().await?.join(dir).join("saves"); @@ -42,3 +44,57 @@ pub async fn get_worlds(cluster: &clusters::Model) -> LauncherResult Ok(list) } + +/// Returns a list of log file names +pub async fn get_logs(cluster: &clusters::Model) -> LauncherResult> { + let dir = cluster.folder_name.clone(); + let path = Dirs::get_clusters_dir().await?.join(dir).join("logs"); + + if !path.exists() { + io::create_dir(&path).await?; + return Ok(Vec::new()); + } + + let mut list = vec![]; + let mut files = io::read_dir(path).await?; + while let Ok(Some(entry)) = files.next_entry().await { + list.push(entry.file_name().to_string_lossy().to_string()); + } + + list.sort_by(|a, b| { + if a == "latest.log" { + std::cmp::Ordering::Less + } else if b == "latest.log" { + std::cmp::Ordering::Greater + } else { + a.cmp(b) + } + }); + + Ok(list) +} + +/// returns a log from a cluster and file name +pub async fn get_log_by_name( + cluster: &clusters::Model, + file_name: &str, +) -> LauncherResult> { + let dir = cluster.folder_name.clone(); + let path = Dirs::get_clusters_dir() + .await? + .join(dir) + .join("logs") + .join(file_name); + + if !path.exists() { + return Ok(None); + } + + let content = if file_name.ends_with(".gz") { + io::read_gz_to_string(&path).await? + } else { + io::read_to_string(&path).await? + }; + + Ok(Some(content)) +} diff --git a/packages/core/src/api/packages/dao.rs b/packages/core/src/api/packages/dao.rs index 2a884a74..ae0be132 100644 --- a/packages/core/src/api/packages/dao.rs +++ b/packages/core/src/api/packages/dao.rs @@ -8,8 +8,6 @@ use sea_orm::{ use crate::error::{DaoError, LauncherResult}; use crate::store::State; -use super::PackageError; - pub type PackageId = String; /// Inserts a new package into the database. diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 143a2e8e..8d9bcd21 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -1,5 +1,6 @@ use crate::api::packages::data::{ - ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult + ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, + SearchResult, }; use crate::api::packages::provider::ProviderExt; use crate::store::{Settings, State}; @@ -48,6 +49,12 @@ pub trait TauriLauncherApi { #[taurpc(alias = "getWorlds")] async fn get_worlds(id: ClusterId) -> LauncherResult>; + #[taurpc(alias = "getLogs")] + async fn get_logs(id: ClusterId) -> LauncherResult>; + + #[taurpc(alias = "getLogByName")] + async fn get_log_by_name(id: ClusterId, name: String) -> LauncherResult>; + // Setting Profiles #[taurpc(alias = "getProfileOrDefault")] async fn get_profile_or_default(name: Option) -> LauncherResult; @@ -109,7 +116,10 @@ pub trait TauriLauncherApi { async fn get_package(provider: Provider, slug: String) -> LauncherResult; #[taurpc(alias = "getPackageBody")] - async fn get_package_body(provider: Provider, body: ManagedPackageBody) -> LauncherResult; + async fn get_package_body( + provider: Provider, + body: ManagedPackageBody, + ) -> LauncherResult; #[taurpc(alias = "getMultiplePackages")] async fn get_multiple_packages( @@ -290,6 +300,22 @@ impl TauriLauncherApi for TauriLauncherApiImpl { api::cluster::content::get_worlds(&cluster).await } + async fn get_logs(self, id: ClusterId) -> LauncherResult> { + let cluster = api::cluster::dao::get_cluster_by_id(id) + .await? + .ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?; + + api::cluster::content::get_logs(&cluster).await + } + + async fn get_log_by_name(self, id: ClusterId, name: String) -> LauncherResult> { + let cluster = api::cluster::dao::get_cluster_by_id(id) + .await? + .ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?; + + api::cluster::content::get_log_by_name(&cluster, &name).await + } + // Setting Profiles async fn get_global_profile(self) -> SettingsProfile { api::setting_profiles::get_global_profile().await @@ -476,7 +502,11 @@ impl TauriLauncherApi for TauriLauncherApiImpl { provider.get(&slug).await } - async fn get_package_body(self, provider: Provider, body: ManagedPackageBody) -> LauncherResult { + async fn get_package_body( + self, + provider: Provider, + body: ManagedPackageBody, + ) -> LauncherResult { provider.get_body(&body).await } diff --git a/packages/core/src/api/tauri/folders.rs b/packages/core/src/api/tauri/folders.rs new file mode 100644 index 00000000..8ec44c97 --- /dev/null +++ b/packages/core/src/api/tauri/folders.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use crate::error::LauncherResult; +use crate::store::Dirs; + +#[taurpc::procedures(path = "folders")] +pub trait TauriLauncherFoldersApi { + #[taurpc(alias = "fromCluster")] + async fn from_cluster(folder_name: String) -> LauncherResult; + + #[taurpc(alias = "openCluster")] + async fn open_cluster(folder_name: String) -> LauncherResult<()>; +} + +#[taurpc::ipc_type] +pub struct TauriLauncherFoldersApiImpl; + +#[taurpc::resolvers] +impl TauriLauncherFoldersApi for TauriLauncherFoldersApiImpl { + async fn from_cluster(self, folder_name: String) -> LauncherResult { + Ok(Dirs::get_clusters_dir().await?.join(folder_name)) + } + + async fn open_cluster(self, folder_name: String) -> LauncherResult<()> { + let path = self.from_cluster(folder_name).await?; + Ok(opener::open(path)?) + } +} diff --git a/packages/core/src/api/tauri/mod.rs b/packages/core/src/api/tauri/mod.rs index 5a467e3c..068c9e89 100644 --- a/packages/core/src/api/tauri/mod.rs +++ b/packages/core/src/api/tauri/mod.rs @@ -3,3 +3,18 @@ pub use events::*; mod commands; pub use commands::*; + +mod folders; +pub use folders::*; + +pub trait TauRPCLauncherExt { + fn use_launcher_api(self) -> Self; +} + +impl TauRPCLauncherExt for taurpc::Router { + fn use_launcher_api(self) -> Self { + self.merge(TauriLauncherApiImpl.into_handler()) + .merge(TauriLauncherEventApiImpl.into_handler()) + .merge(TauriLauncherFoldersApiImpl.into_handler()) + } +} diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 51daba92..0817b4b9 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -74,6 +74,12 @@ pub enum LauncherError { #[skip] url::ParseError, ), + #[error(transparent)] + OpenerError( + #[from] + #[skip] + opener::OpenError, + ), #[cfg(feature = "tauri")] #[error(transparent)] diff --git a/packages/web_common/src/components/Dropdown.tsx b/packages/web_common/src/components/Dropdown.tsx index 0269f048..336ffe32 100644 --- a/packages/web_common/src/components/Dropdown.tsx +++ b/packages/web_common/src/components/Dropdown.tsx @@ -105,6 +105,9 @@ export interface DropdownProps customMinimalIcon?: React.ReactNode; popoverClassName?: string; selectValue?: (arg1: SelectValueRenderProps & { defaultChildren: React.ReactNode | undefined }) => string; + + triggerClassName?: string; + triggerContentClassName?: string; } export function Dropdown(props: DropdownProps) { @@ -119,6 +122,8 @@ export function Dropdown(props: DropdownProps) { minimal, popoverClassName, selectValue, + triggerClassName, + triggerContentClassName, ...rest } = props; @@ -130,8 +135,8 @@ export function Dropdown(props: DropdownProps) { <> {label && {label}} - - +
- + {children} diff --git a/packages/web_common/src/hooks/useCommand.ts b/packages/web_common/src/hooks/useCommand.ts index 0d65ea33..9aab92fa 100644 --- a/packages/web_common/src/hooks/useCommand.ts +++ b/packages/web_common/src/hooks/useCommand.ts @@ -1,18 +1,31 @@ /* eslint-disable query/exhaustive-deps -- False */ import type { UndefinedInitialDataOptions, UseMutationOptions, UseMutationResult, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult } from '@tanstack/react-query'; +import type { AppError } from '../utils/error'; import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { isLauncherError } from '../utils/error'; // this gets overwritten by the consumer project export interface Register { - // commands + // commands: string[]; + // defaultError: Error; } -type RegisterConfig = Register extends { +type CommandKeys = Register extends { commands: infer Keys; } ? Keys : never; -type CommandCacheKey = RegisterConfig[number] | (string & {}); -type CommandCacheKeys = Array | [CommandCacheKey] | [false]; +type DefaultError = Register extends { + defaultError: infer Error; +} ? Error : never; + +type QueryKey = [CommandKeys[number], ...ReadonlyArray]; + +declare module '@tanstack/react-query' { + interface Register { + queryKey: QueryKey; + mutationKey: QueryKey; + } +} async function fetchCommand(command: () => Promise): Promise { try { @@ -21,40 +34,40 @@ async function fetchCommand(command: () => Promise): Promise { } // TODO: (lynith) handle errors better (requires modifications to TauRPC) catch (e) { - console.error(e); - if (e instanceof Error) + console.error('[fetch]', e); + if (isLauncherError(e)) return Promise.reject(e); else - return Promise.reject(new Error('Unknown error')); + return Promise.reject(e); } } -export function useCommand( - cacheKey: CommandCacheKeys, +export function useCommand( + cacheKey: TQueryKey, command: () => Promise, options?: Omit< - UndefinedInitialDataOptions, + UndefinedInitialDataOptions, 'queryKey' | 'queryFn' >, -): UseQueryResult { - return useQuery({ - queryKey: cacheKey[0] === false ? [] : Array.isArray(cacheKey) ? cacheKey : [cacheKey], +): UseQueryResult { + return useQuery({ + queryKey: cacheKey, queryFn: () => fetchCommand(command), enabled: true, ...options, }); } -export function useCommandSuspense( - cacheKey: CommandCacheKeys, +export function useCommandSuspense( + cacheKey: TQueryKey, command: () => Promise, options?: Omit< - UseSuspenseQueryOptions, + UseSuspenseQueryOptions, 'queryKey' | 'queryFn' >, -): UseSuspenseQueryResult { - return useSuspenseQuery({ - queryKey: cacheKey[0] === false ? [] : Array.isArray(cacheKey) ? cacheKey : [cacheKey], +): UseSuspenseQueryResult { + return useSuspenseQuery({ + queryKey: cacheKey, queryFn: () => fetchCommand(command), ...options, }); @@ -62,7 +75,7 @@ export function useCommandSuspense( export function useCommandMut< TData, - TError = Error, + TError = DefaultError, TVariables = void, TContext = unknown, >( diff --git a/packages/web_common/src/utils/error.ts b/packages/web_common/src/utils/error.ts new file mode 100644 index 00000000..d6ccb73d --- /dev/null +++ b/packages/web_common/src/utils/error.ts @@ -0,0 +1,23 @@ +export interface AppError { + data: string | AppError; + type: string; +} + +export function isLauncherError(error: unknown): error is TErr { + return typeof error === 'object' + && error !== null + && 'data' in error + && 'type' in error; +} + +export function getMessageFromError(error: T): string; +export function getMessageFromError(error: T | null | undefined): string | null; +export function getMessageFromError(error: AppError | null | undefined): string | null { + if (!error) + return null; + + if (typeof error.data === 'string') + return error.data; + + return getMessageFromError(error.data); +} diff --git a/packages/web_common/src/utils/index.ts b/packages/web_common/src/utils/index.ts index 67b87f0c..acdeae59 100644 --- a/packages/web_common/src/utils/index.ts +++ b/packages/web_common/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './dev'; +export * from './error'; export * from './nativeExperience'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fc21ce7..75dd339f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ catalogs: '@tanstack/match-sorter-utils': specifier: ^8.19.4 version: 8.19.4 + '@tanstack/react-devtools': + specifier: ^0.4.0 + version: 0.4.0 '@tanstack/react-query': specifier: ^5.66.5 version: 5.75.0 @@ -232,6 +235,9 @@ importers: '@tanstack/match-sorter-utils': specifier: 'catalog:' version: 8.19.4 + '@tanstack/react-devtools': + specifier: 'catalog:' + version: 0.4.0(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.6) '@tanstack/react-query': specifier: 'catalog:' version: 5.75.0(react@19.1.0) @@ -2480,6 +2486,26 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@solid-primitives/event-listener@2.4.3': + resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/keyboard@1.3.3': + resolution: {integrity: sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/rootless@1.5.2': + resolution: {integrity: sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.3.2': + resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} + peerDependencies: + solid-js: ^1.6.12 + '@stylistic/eslint-plugin-js@4.2.0': resolution: {integrity: sha512-MiJr6wvyzMYl/wElmj8Jns8zH7Q1w8XoVtm+WM6yDaTrfxryMyb8n0CMxt82fo42RoLIfxAEtM6tmQVxqhk0/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2658,6 +2684,22 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 + '@tanstack/devtools-event-bus@0.2.1': + resolution: {integrity: sha512-JMq3AmrQR2LH9P8Rcj1MTq8Iq/mPk/PyuqSw1L0hO2Wl8G1oz5ue31fS8u8lIgOCVR/mGdJah18p+Pj5OosRJA==} + engines: {node: '>=18'} + + '@tanstack/devtools-ui@0.3.0': + resolution: {integrity: sha512-lyP0eM6juIWn8zgI8xI32Lh86gCnjUyNePE9F7Bfgkv5taILmmJAHW5Mme4T2ufv7L8NLwOiBY/bZYnP4zev0w==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/devtools@0.4.0': + resolution: {integrity: sha512-3LrVUMI/s76XxYbygecOueXPrUf9gDc700iebQrBZ0ZvDmZDB/Umwa84gNx6NAxfQyI24lvB0IqQ8lp970oDSg==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + '@tanstack/eslint-config@0.1.0': resolution: {integrity: sha512-/lVsQmmezpqqbCFtaxGNkoSnQ/+H0PAHyHF37PGQ6ZBrF8RXktgazMdsD5SCaNT9oEnGCl274GCZ9sY80b50Sg==} engines: {node: '>=18'} @@ -2681,6 +2723,15 @@ packages: '@tanstack/query-devtools@5.74.7': resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==} + '@tanstack/react-devtools@0.4.0': + resolution: {integrity: sha512-QkN0WcbwKjYa107yEYOau+yhaay3XXwL2Ue+V0JxzaBOhvbnFI9tDzvIx7T1NZs8qEUgSjsqmMj/H81cISs2EA==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=16.8' + '@types/react-dom': '>=16.8' + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-query-devtools@5.75.0': resolution: {integrity: sha512-ogTsCCONHPq8opyeU/NJZlus8+g36KnK+cybC1cs0Knr8iO/wAYgnciz+3DVNq38kRflBb0iJhHkUexp/7WfcQ==} peerDependencies: @@ -5964,6 +6015,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -8337,6 +8400,27 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@solid-primitives/event-listener@2.4.3(solid-js@1.9.6)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.6) + solid-js: 1.9.6 + + '@solid-primitives/keyboard@1.3.3(solid-js@1.9.6)': + dependencies: + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.6) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.6) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.6) + solid-js: 1.9.6 + + '@solid-primitives/rootless@1.5.2(solid-js@1.9.6)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.6) + solid-js: 1.9.6 + + '@solid-primitives/utils@6.3.2(solid-js@1.9.6)': + dependencies: + solid-js: 1.9.6 + '@stylistic/eslint-plugin-js@4.2.0(eslint@9.25.1(jiti@2.4.2))': dependencies: eslint: 9.25.1(jiti@2.4.2) @@ -8508,6 +8592,33 @@ snapshots: tailwindcss: 4.1.10 vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0) + '@tanstack/devtools-event-bus@0.2.1': + dependencies: + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-ui@0.3.0(csstype@3.1.3)(solid-js@1.9.6)': + dependencies: + goober: 2.1.16(csstype@3.1.3) + solid-js: 1.9.6 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools@0.4.0(csstype@3.1.3)(solid-js@1.9.6)': + dependencies: + '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.6) + '@tanstack/devtools-event-bus': 0.2.1 + '@tanstack/devtools-ui': 0.3.0(csstype@3.1.3)(solid-js@1.9.6) + clsx: 2.1.1 + goober: 2.1.16(csstype@3.1.3) + solid-js: 1.9.6 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + '@tanstack/eslint-config@0.1.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint/js': 9.25.1 @@ -8541,6 +8652,19 @@ snapshots: '@tanstack/query-devtools@5.74.7': {} + '@tanstack/react-devtools@0.4.0(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.6)': + dependencies: + '@tanstack/devtools': 0.4.0(csstype@3.1.3)(solid-js@1.9.6) + '@types/react': 19.1.2 + '@types/react-dom': 19.1.3(@types/react@19.1.2) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - bufferutil + - csstype + - solid-js + - utf-8-validate + '@tanstack/react-query-devtools@5.75.0(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/query-devtools': 5.74.7 @@ -12439,6 +12563,8 @@ snapshots: ws@8.18.0: optional: true + ws@8.18.3: {} + xml-name-validator@5.0.0: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 54eedc00..87c122f5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -61,6 +61,7 @@ catalog: '@tanstack/react-table': ^8.21.2 '@tanstack/router-plugin': ^1.114.3 '@tanstack/react-virtual': ^3.13.12 + '@tanstack/react-devtools': ^0.4.0 # tauri taurpc: ^1.8.1 From fdf74258a065dd4c89df333cc17f5591afd0f3c9 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:21:27 +0200 Subject: [PATCH 3/3] fix: get_logs returning folders as well --- apps/oneclient/frontend/src/bindings.gen.ts | 16 ++++++++-------- .../frontend/src/routes/app/cluster/logs.tsx | 2 +- .../frontend/src/routes/app/cluster/overview.tsx | 6 ++++-- .../frontend/src/routes/app/cluster/route.tsx | 2 -- .../frontend/src/routes/app/clusters.tsx | 4 ++-- apps/oneclient/frontend/src/routes/app/index.tsx | 2 +- packages/core/src/api/cluster/content.rs | 4 +++- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 8ab8bfaa..26d7cfcf 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -228,13 +228,8 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"openMsaLogin":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getMultiplePackages":["provider","slugs"],"getGlobalProfile":[],"getLogByName":["id","name"],"getLogs":["id"],"getClusterById":["id"],"getLoadersForVersion":["mc_version"],"getDefaultUser":["fallback"],"getWorlds":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"getUser":["uuid"],"launchCluster":["id","uuid"],"getPackageBody":["provider","body"],"readSettings":[],"getGameVersions":[],"searchPackages":["provider","query"],"getUsers":[],"removeUser":["uuid"],"createCluster":["options"],"writeSettings":["setting"],"getPackage":["provider","slug"],"setDefaultUser":["uuid"],"getScreenshots":["id"],"updateClusterById":["id","request"],"getClusters":[],"removeCluster":["id"]}' } -export type Router = { 'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise> }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'core': { getClusters: () => Promise, +const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"openMsaLogin":[],"readSettings":[],"getProfileOrDefault":["name"],"getLogByName":["id","name"],"getWorlds":["id"],"launchCluster":["id","uuid"],"getGameVersions":[],"getUser":["uuid"],"removeUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getPackage":["provider","slug"],"setDefaultUser":["uuid"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"getGlobalProfile":[],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"writeSettings":["setting"],"getScreenshots":["id"],"getLogs":["id"],"getDefaultUser":["fallback"],"getClusterById":["id"],"removeCluster":["id"],"updateClusterProfile":["name","profile"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"getClusters":[],"searchPackages":["provider","query"],"getUsers":[]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } +export type Router = { 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, @@ -265,7 +260,12 @@ getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise, getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise }, 'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise } }; +openCluster: (folderName: string) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise> }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx b/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx index 074f5caa..d7c98178 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/logs.tsx @@ -15,7 +15,7 @@ function RouteComponent() { const { cluster } = Route.useRouteContext(); const { scrollContainerRef } = useSheetPageContext(); - const { data: fileNames } = useCommandSuspense(['getLogs', cluster.id.toString()], () => bindings.core.getLogs(cluster.id)); + const { data: fileNames } = useCommandSuspense(['getLogs', cluster.id], () => bindings.core.getLogs(cluster.id)); const [activeFileName, setActiveFileName] = useState(fileNames[0] || undefined); diff --git a/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx b/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx index 961c179c..be38ff7a 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx @@ -5,8 +5,10 @@ import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/app/cluster/overview')({ component: RouteComponent, async beforeLoad(ctx) { - const cluster = await bindings.core.getClusterById(ctx.search.clusterId as unknown as bigint); - return { cluster }; + const cluster = await bindings.core.getClusterById(ctx.search.clusterId); + return { + cluster, + }; }, }); diff --git a/apps/oneclient/frontend/src/routes/app/cluster/route.tsx b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx index 26e82c79..c2c887b0 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/route.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx @@ -11,7 +11,6 @@ import { useRef, useState } from 'react'; export interface ClusterRouteSearchParams { clusterId: number; - a: number; } export const Route = createFileRoute('/app/cluster')({ @@ -19,7 +18,6 @@ export const Route = createFileRoute('/app/cluster')({ validateSearch: (search): ClusterRouteSearchParams => { return { clusterId: Number(search.clusterId), - a: 6, }; }, async beforeLoad({ context, search }) { diff --git a/apps/oneclient/frontend/src/routes/app/clusters.tsx b/apps/oneclient/frontend/src/routes/app/clusters.tsx index a003a87d..60d0076c 100644 --- a/apps/oneclient/frontend/src/routes/app/clusters.tsx +++ b/apps/oneclient/frontend/src/routes/app/clusters.tsx @@ -24,7 +24,7 @@ function RouteComponent() { const navigate = Route.useNavigate(); - const { data: clusters } = useCommandSuspense('getClustersGroupedByMajor', bindings.oneclient.getClustersGroupedByMajor); + const { data: clusters } = useCommandSuspense(['getClustersGroupedByMajor'], bindings.oneclient.getClustersGroupedByMajor); const loadMinorVersion = useCallback((major: number) => { const existing = prevMinorVersions.find(v => v.major === major); @@ -194,7 +194,7 @@ function RouteComponent() { navigate({ to: `/app/cluster/overview`, search: { - clusterId: cluster.id as unknown as number, + clusterId: cluster.id, }, }); }} diff --git a/apps/oneclient/frontend/src/routes/app/index.tsx b/apps/oneclient/frontend/src/routes/app/index.tsx index c15fc4e0..41a17146 100644 --- a/apps/oneclient/frontend/src/routes/app/index.tsx +++ b/apps/oneclient/frontend/src/routes/app/index.tsx @@ -19,7 +19,7 @@ export const Route = createFileRoute('/app/')({ function RouteComponent() { const { data: clusters } = useCommandSuspense( - 'getClusters', + ['getClusters'], bindings.core.getClusters, { select(data) { diff --git a/packages/core/src/api/cluster/content.rs b/packages/core/src/api/cluster/content.rs index daba14c8..5476527e 100644 --- a/packages/core/src/api/cluster/content.rs +++ b/packages/core/src/api/cluster/content.rs @@ -58,7 +58,9 @@ pub async fn get_logs(cluster: &clusters::Model) -> LauncherResult> let mut list = vec![]; let mut files = io::read_dir(path).await?; while let Ok(Some(entry)) = files.next_entry().await { - list.push(entry.file_name().to_string_lossy().to_string()); + if entry.file_type().await.is_ok_and(|ft| ft.is_file()) { + list.push(entry.file_name().to_string_lossy().to_string()); + } } list.sort_by(|a, b| {