feat: log viewer
This commit is contained in:
parent
843db8f995
commit
6d4e0fa850
22 changed files with 541 additions and 109 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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<tauri::Wry>) -> 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| {
|
||||
|
|
|
|||
|
|
@ -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:",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ type TAURI_CHANNEL<T> = (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<T> = { total: bigint; offset: bigint; limit: bigint; items: T[] }
|
||||
export type Paginated<T> = { 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<string, never>
|
||||
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<ClusterModel[]>,
|
||||
getClusterById: (id: bigint) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: bigint) => Promise<null>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
launchCluster: (id: bigint, uuid: string | null) => Promise<null>,
|
||||
updateClusterById: (id: bigint, request: ClusterUpdate) => Promise<null>,
|
||||
getScreenshots: (id: bigint) => Promise<string[]>,
|
||||
getWorlds: (id: bigint) => Promise<string[]>,
|
||||
launchCluster: (id: number, uuid: string | null) => Promise<null>,
|
||||
updateClusterById: (id: number, request: ClusterUpdate) => Promise<null>,
|
||||
getScreenshots: (id: number) => Promise<string[]>,
|
||||
getWorlds: (id: number) => Promise<string[]>,
|
||||
getLogs: (id: number) => Promise<string[]>,
|
||||
getLogByName: (id: number, name: string) => Promise<string | null>,
|
||||
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
|
||||
getGlobalProfile: () => Promise<SettingProfileModel>,
|
||||
updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise<SettingProfileModel>,
|
||||
|
|
@ -254,14 +256,16 @@ searchPackages: (provider: Provider, query: SearchQuery) => Promise<Paginated<Se
|
|||
getPackage: (provider: Provider, slug: string) => Promise<ManagedPackage>,
|
||||
getPackageBody: (provider: Provider, body: ManagedPackageBody) => Promise<string>,
|
||||
getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPackage[]>,
|
||||
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: bigint, limit: bigint) => Promise<Paginated<ManagedVersion>>,
|
||||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: bigint, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
|
||||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
56
apps/oneclient/frontend/src/components/LogViewer.module.css
Normal file
56
apps/oneclient/frontend/src/components/LogViewer.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
110
apps/oneclient/frontend/src/components/LogViewer.tsx
Normal file
110
apps/oneclient/frontend/src/components/LogViewer.tsx
Normal file
|
|
@ -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<HTMLElement | null>;
|
||||
ref?: React.RefObject<LogViewerRef>;
|
||||
}
|
||||
|
||||
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<Array<string>>(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 (
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<OverlayScrollbarsComponent
|
||||
className={styles.log_container}
|
||||
options={{
|
||||
overflow: {
|
||||
x: 'scroll',
|
||||
y: 'hidden',
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
<code className={styles.log}>
|
||||
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||
<LogLine
|
||||
key={virtualRow.key}
|
||||
line={linesRef.current[virtualRow.index]}
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</code>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span style={style}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
|
||||
const [matched, time, threadName, level, isChat] = groups as [string, string, string, string, string | undefined];
|
||||
|
||||
return (
|
||||
<span data-level={level} style={style}>
|
||||
<span data-time={time}>[{time}]</span>
|
||||
<span>[{threadName}/{level}]</span>
|
||||
<span data-chat={isChat}>{isChat ? ' [CHAT] ' : ' '}{line.slice(matched.length)}</span>
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export interface SheetPageContextApi {
|
||||
scrollContainerRef: React.RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
const SheetPageContext = React.createContext<SheetPageContextApi | null>(null);
|
||||
|
||||
export function SheetPage({
|
||||
headerSmall,
|
||||
headerLarge,
|
||||
children,
|
||||
scrollContainerRef,
|
||||
}: {
|
||||
headerSmall: React.ReactNode;
|
||||
headerLarge: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
||||
}) {
|
||||
}: SheetPageProps) {
|
||||
const scrollContainer = useRef<HTMLElement>(null);
|
||||
const headerLargeRef = useRef<HTMLDivElement>(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<SheetPageContextApi>(() => ({
|
||||
scrollContainerRef: scrollContainer,
|
||||
}), [scrollContainer]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<HeaderBackgrounds scrollYProgress={scrollYProgress} />
|
||||
|
|
@ -63,7 +75,9 @@ export function SheetPage({
|
|||
{headerLarge}
|
||||
</motion.div>
|
||||
|
||||
{children}
|
||||
<SheetPageContext.Provider value={context}>
|
||||
{children}
|
||||
</SheetPageContext.Provider>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<Key | undefined>(fileNames[0] || undefined);
|
||||
|
||||
return (
|
||||
<SheetPage.Content>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xxl font-semibold">Logs</h2>
|
||||
<span className="flex font-mono items-center rounded-md bg-component-bg py-1.5 px-2 gap-1">
|
||||
<File05Icon width={20} />
|
||||
{logFile.fileName}
|
||||
</span>
|
||||
<h2 className="flex-1 text-xxl font-semibold">Logs</h2>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<Dropdown onSelectionChange={e => setActiveFileName(e)} selectedKey={activeFileName}>
|
||||
{fileNames.map(fileName => (
|
||||
<Dropdown.Item id={fileName} key={fileName}>
|
||||
{fileName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<code className="font-mono p-4 bg-component-bg whitespace-pre-line rounded-xl">
|
||||
{logFile.content}
|
||||
</code>
|
||||
{/* Additional content can be added here */}
|
||||
|
||||
{activeFileName && (<LogContent fileName={activeFileName.toString()} scrollRef={scrollContainerRef} />)}
|
||||
</div>
|
||||
</SheetPage.Content>
|
||||
);
|
||||
}
|
||||
|
||||
function LogContent({
|
||||
fileName,
|
||||
scrollRef,
|
||||
}: {
|
||||
fileName: string;
|
||||
scrollRef: React.RefObject<HTMLElement | null>;
|
||||
}) {
|
||||
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 && <LogViewer content={content} scrollRef={scrollRef} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</TabList>
|
||||
|
||||
<div className="relative">
|
||||
<Outlet />
|
||||
<LoaderSuspense spinner={{ size: 'large' }}>
|
||||
<Outlet />
|
||||
</LoaderSuspense>
|
||||
</div>
|
||||
</SheetPage>
|
||||
);
|
||||
|
|
@ -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 (
|
||||
<div className="flex flex-row justify-between items-end gap-16">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row items-end gap-16">
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2 flex-wrap">
|
||||
{versionInfo.tags.map(tag => (
|
||||
<span className="text-sm font-medium text-fg-secondary bg-component-bg/40 px-2 py-1 rounded" key={tag}>
|
||||
|
|
@ -99,13 +102,23 @@ function HeaderLarge() {
|
|||
<p className="text-md font-medium text-fg-secondary">{versionInfo.longDescription}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={launch}
|
||||
size="large"
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
onPress={openFolder}
|
||||
size="iconLarge"
|
||||
>
|
||||
<FolderIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={launch}
|
||||
size="large"
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="flex flex-row justify-between items-center h-full">
|
||||
<h1 className="text-2lg h-full font-medium">{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}</h1>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={launch}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
onPress={openFolder}
|
||||
size="icon"
|
||||
>
|
||||
<FolderIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={launch}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<Vec<String>> {
|
||||
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<Vec<St
|
|||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of world filenames
|
||||
pub async fn get_worlds(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
|
||||
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<Vec<String>
|
|||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of log file names
|
||||
pub async fn get_logs(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
|
||||
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<Option<String>> {
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<Vec<String>>;
|
||||
|
||||
#[taurpc(alias = "getLogs")]
|
||||
async fn get_logs(id: ClusterId) -> LauncherResult<Vec<String>>;
|
||||
|
||||
#[taurpc(alias = "getLogByName")]
|
||||
async fn get_log_by_name(id: ClusterId, name: String) -> LauncherResult<Option<String>>;
|
||||
|
||||
// Setting Profiles
|
||||
#[taurpc(alias = "getProfileOrDefault")]
|
||||
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
|
||||
|
|
@ -109,7 +116,10 @@ pub trait TauriLauncherApi {
|
|||
async fn get_package(provider: Provider, slug: String) -> LauncherResult<ManagedPackage>;
|
||||
|
||||
#[taurpc(alias = "getPackageBody")]
|
||||
async fn get_package_body(provider: Provider, body: ManagedPackageBody) -> LauncherResult<String>;
|
||||
async fn get_package_body(
|
||||
provider: Provider,
|
||||
body: ManagedPackageBody,
|
||||
) -> LauncherResult<String>;
|
||||
|
||||
#[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<Vec<String>> {
|
||||
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<Option<String>> {
|
||||
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<String> {
|
||||
async fn get_package_body(
|
||||
self,
|
||||
provider: Provider,
|
||||
body: ManagedPackageBody,
|
||||
) -> LauncherResult<String> {
|
||||
provider.get_body(&body).await
|
||||
}
|
||||
|
||||
|
|
|
|||
28
packages/core/src/api/tauri/folders.rs
Normal file
28
packages/core/src/api/tauri/folders.rs
Normal file
|
|
@ -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<PathBuf>;
|
||||
|
||||
#[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<PathBuf> {
|
||||
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)?)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<R: tauri::Runtime> TauRPCLauncherExt for taurpc::Router<R> {
|
||||
fn use_launcher_api(self) -> Self {
|
||||
self.merge(TauriLauncherApiImpl.into_handler())
|
||||
.merge(TauriLauncherEventApiImpl.into_handler())
|
||||
.merge(TauriLauncherFoldersApiImpl.into_handler())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,12 @@ pub enum LauncherError {
|
|||
#[skip]
|
||||
url::ParseError,
|
||||
),
|
||||
#[error(transparent)]
|
||||
OpenerError(
|
||||
#[from]
|
||||
#[skip]
|
||||
opener::OpenError,
|
||||
),
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
#[error(transparent)]
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export interface DropdownProps<T extends object>
|
|||
customMinimalIcon?: React.ReactNode;
|
||||
popoverClassName?: string;
|
||||
selectValue?: (arg1: SelectValueRenderProps<object> & { defaultChildren: React.ReactNode | undefined }) => string;
|
||||
|
||||
triggerClassName?: string;
|
||||
triggerContentClassName?: string;
|
||||
}
|
||||
|
||||
export function Dropdown<TValue extends object>(props: DropdownProps<TValue>) {
|
||||
|
|
@ -119,6 +122,8 @@ export function Dropdown<TValue extends object>(props: DropdownProps<TValue>) {
|
|||
minimal,
|
||||
popoverClassName,
|
||||
selectValue,
|
||||
triggerClassName,
|
||||
triggerContentClassName,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
|
|
@ -130,8 +135,8 @@ export function Dropdown<TValue extends object>(props: DropdownProps<TValue>) {
|
|||
<>
|
||||
{label && <AriaLabel className={labelClassName}>{label}</AriaLabel>}
|
||||
|
||||
<Button className={styles.trigger()} color="ghost">
|
||||
<div className={styles.triggerValueContent()}>
|
||||
<Button className={styles.trigger({ className: triggerClassName })} color="ghost">
|
||||
<div className={styles.triggerValueContent({ className: triggerContentClassName })}>
|
||||
{triggerTextPrefix && <span>{triggerTextPrefix}</span>}
|
||||
{selectValue
|
||||
? (
|
||||
|
|
@ -177,7 +182,7 @@ Dropdown.Item = (props: DropdownItemProps) => {
|
|||
const { className, children } = props;
|
||||
|
||||
return (
|
||||
<AriaListBoxItem {...props} className={twMerge(itemStyles.container(), className?.toString())}>
|
||||
<AriaListBoxItem aria-label="dropdown item" {...props} className={twMerge(itemStyles.container(), className?.toString())}>
|
||||
{children}
|
||||
</AriaListBoxItem>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ type RegisterConfig = Register extends {
|
|||
commands: infer Keys;
|
||||
} ? Keys : never;
|
||||
|
||||
type CommandCacheKey = RegisterConfig[number] | (string & {}) | false;
|
||||
type CommandCacheKey = RegisterConfig[number] | (string & {});
|
||||
type CommandCacheKeys = Array<CommandCacheKey> | [CommandCacheKey] | [false];
|
||||
|
||||
async function fetchCommand<T>(command: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
|
|
@ -29,15 +30,15 @@ async function fetchCommand<T>(command: () => Promise<T>): Promise<T> {
|
|||
}
|
||||
|
||||
export function useCommand<TQueryFnData, TSelected = TQueryFnData>(
|
||||
cacheKey: CommandCacheKey,
|
||||
cacheKey: CommandCacheKeys,
|
||||
command: () => Promise<TQueryFnData>,
|
||||
options?: Omit<
|
||||
UndefinedInitialDataOptions<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>,
|
||||
UndefinedInitialDataOptions<TQueryFnData, Error, TSelected, CommandCacheKeys | []>,
|
||||
'queryKey' | 'queryFn'
|
||||
>,
|
||||
): UseQueryResult<TSelected, Error> {
|
||||
return useQuery<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>({
|
||||
queryKey: cacheKey === false ? [] : [cacheKey],
|
||||
return useQuery<TQueryFnData, Error, TSelected, CommandCacheKeys | []>({
|
||||
queryKey: cacheKey[0] === false ? [] : Array.isArray(cacheKey) ? cacheKey : [cacheKey],
|
||||
queryFn: () => fetchCommand(command),
|
||||
enabled: true,
|
||||
...options,
|
||||
|
|
@ -45,15 +46,15 @@ export function useCommand<TQueryFnData, TSelected = TQueryFnData>(
|
|||
}
|
||||
|
||||
export function useCommandSuspense<TQueryFnData, TSelected = TQueryFnData>(
|
||||
cacheKey: CommandCacheKey,
|
||||
cacheKey: CommandCacheKeys,
|
||||
command: () => Promise<TQueryFnData>,
|
||||
options?: Omit<
|
||||
UseSuspenseQueryOptions<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>,
|
||||
UseSuspenseQueryOptions<TQueryFnData, Error, TSelected, CommandCacheKeys | []>,
|
||||
'queryKey' | 'queryFn'
|
||||
>,
|
||||
): UseSuspenseQueryResult<TSelected, Error> {
|
||||
return useSuspenseQuery<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>({
|
||||
queryKey: cacheKey === false ? [] : [cacheKey],
|
||||
return useSuspenseQuery<TQueryFnData, Error, TSelected, CommandCacheKeys | []>({
|
||||
queryKey: cacheKey[0] === false ? [] : Array.isArray(cacheKey) ? cacheKey : [cacheKey],
|
||||
queryFn: () => fetchCommand(command),
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
|
|
@ -42,6 +42,9 @@ catalogs:
|
|||
'@tanstack/react-table':
|
||||
specifier: ^8.21.2
|
||||
version: 8.21.3
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.12
|
||||
version: 3.13.12
|
||||
'@tanstack/router-plugin':
|
||||
specifier: ^1.114.3
|
||||
version: 1.119.0
|
||||
|
|
@ -244,6 +247,9 @@ importers:
|
|||
'@tanstack/react-table':
|
||||
specifier: 'catalog:'
|
||||
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@tanstack/react-virtual':
|
||||
specifier: 'catalog:'
|
||||
version: 3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@tanstack/router-plugin':
|
||||
specifier: 'catalog:'
|
||||
version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(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)))(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))
|
||||
|
|
@ -256,9 +262,6 @@ importers:
|
|||
'@tauri-apps/plugin-dialog':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.1
|
||||
'@untitled-theme/icons-react':
|
||||
specifier: 'catalog:'
|
||||
version: 0.14.0(@types/react@19.1.2)(react@19.1.0)
|
||||
|
|
@ -2717,6 +2720,12 @@ packages:
|
|||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
'@tanstack/react-virtual@3.13.12':
|
||||
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/router-core@1.119.0':
|
||||
resolution: {integrity: sha512-3dZYP5cCq3jJYgnRDzKR3w4sYzrXP5sw1st303ye87VV26r31I8UaIuUEs7kiJaxgWBvqHglWCiygBWQODZXVw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -2774,6 +2783,9 @@ packages:
|
|||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/virtual-core@3.13.12':
|
||||
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.115.0':
|
||||
resolution: {integrity: sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -8576,6 +8588,12 @@ snapshots:
|
|||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@tanstack/react-virtual@3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.12
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@tanstack/router-core@1.119.0':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.115.0
|
||||
|
|
@ -8638,6 +8656,8 @@ snapshots:
|
|||
|
||||
'@tanstack/table-core@8.21.3': {}
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.115.0': {}
|
||||
|
||||
'@tauri-apps/api@2.5.0': {}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@ catalog:
|
|||
# react + libraries
|
||||
react: ^19.0.0
|
||||
react-aria-components: ^1.8.0
|
||||
'@tanstack/react-query': ^5.66.5
|
||||
'@tanstack/react-router': ^1.114.3
|
||||
'@untitled-theme/icons-react': ^0.14.0
|
||||
motion: ^12.10.5
|
||||
react-dom: ^19.0.0
|
||||
|
|
@ -58,8 +56,11 @@ catalog:
|
|||
'@tanstack/match-sorter-utils': ^8.19.4
|
||||
'@tanstack/react-query-devtools': ^5.66.5
|
||||
'@tanstack/react-router-devtools': ^1.114.3
|
||||
'@tanstack/react-query': ^5.66.5
|
||||
'@tanstack/react-router': ^1.114.3
|
||||
'@tanstack/react-table': ^8.21.2
|
||||
'@tanstack/router-plugin': ^1.114.3
|
||||
'@tanstack/react-virtual': ^3.13.12
|
||||
|
||||
# tauri
|
||||
taurpc: ^1.8.1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue