diff --git a/apps/desktop/src/api/commands/cluster.rs b/apps/desktop/src/api/commands/cluster.rs index 4a3a2215..6e47ec6e 100644 --- a/apps/desktop/src/api/commands/cluster.rs +++ b/apps/desktop/src/api/commands/cluster.rs @@ -1,5 +1,4 @@ -use std::path::PathBuf; -use std::str::FromStr; +use std::{collections::HashMap, path::PathBuf, str::FromStr}; use onelauncher::cluster::content::logger; use onelauncher::cluster::{self}; @@ -25,6 +24,12 @@ pub async fn get_clusters() -> Result, String> { Ok(onelauncher::cluster::list(None).await?) } +#[specta::specta] +#[tauri::command] +pub async fn get_clusters_grouped() -> Result>, String> { + Ok(onelauncher::cluster::list_grouped(None).await?) +} + #[derive(Serialize, Deserialize, Type)] pub struct CreateCluster { name: String, diff --git a/apps/desktop/src/api/commands/mod.rs b/apps/desktop/src/api/commands/mod.rs index 57d74881..6c22866d 100644 --- a/apps/desktop/src/api/commands/mod.rs +++ b/apps/desktop/src/api/commands/mod.rs @@ -33,6 +33,7 @@ macro_rules! collect_commands { remove_cluster, get_cluster, get_clusters, + get_clusters_grouped, run_cluster, get_cluster_logs, get_cluster_log, diff --git a/apps/frontend/src/ui/hooks/useCommand.ts b/apps/frontend/src/ui/hooks/useCommand.ts index 53e4777c..47194e22 100644 --- a/apps/frontend/src/ui/hooks/useCommand.ts +++ b/apps/frontend/src/ui/hooks/useCommand.ts @@ -6,7 +6,7 @@ import type { Result } from '~bindings'; * @param cmd The command to call * @throws */ -export default function useCommand( +export default function useCommand( cmd: (...args: Args) => Promise>, ...args: Args ): ResourceReturn { diff --git a/apps/frontend/src/ui/pages/Home.tsx b/apps/frontend/src/ui/pages/Home.tsx index caeb0b94..384c6123 100644 --- a/apps/frontend/src/ui/pages/Home.tsx +++ b/apps/frontend/src/ui/pages/Home.tsx @@ -1,5 +1,6 @@ import { CheckIcon, + ChevronRightIcon, DotsVerticalIcon, EyeIcon, PlayIcon, @@ -7,24 +8,39 @@ import { SearchMdIcon, } from '@untitled-theme/icons-solid'; import type { Accessor, Setter } from 'solid-js'; -import { For, Match, Switch, createSignal } from 'solid-js'; +import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'; import { useNavigate } from '@solidjs/router'; +import { + DragDropProvider, + DragDropSensors, + type DragEventHandler, + DragOverlay, + SortableProvider, + createDroppable, + createSortable, +} from '@thisbeyond/solid-dnd'; +import { mergeRefs } from '@solid-primitives/refs'; import image from '../../assets/images/header.png'; import ContextMenu from '../components/overlay/ContextMenu'; import Button from '../components/base/Button'; import Tag from '../components/base/Tag'; import TextField from '../components/base/TextField'; +import ClusterRoot from './cluster/ClusterRoot'; import ClusterCover from '~ui/components/game/ClusterCover'; import type { Cluster } from '~bindings'; import useCommand from '~ui/hooks/useCommand'; import { bridge } from '~imports'; -import { upperFirst } from '~utils/primitives'; +import { secondsToWords, upperFirst } from '~utils/primitives'; import { useClusterModalController } from '~ui/components/overlay/cluster/ClusterCreationModal'; +type GroupedClusters = Record; + function HomePage() { - const [clusters, { refetch }] = useCommand(bridge.commands.getClusters); + const [clusters, { refetch }] = useCommand(bridge.commands.getClustersGrouped); const controller = useClusterModalController(); + const containerIds = (list: GroupedClusters | undefined) => Object.keys(list || []); + async function newCluster() { try { await controller.start(); @@ -36,9 +52,71 @@ function HomePage() { refetch(); } + // function getContainerByCluster(id: string) { + // const list = clusters(); + + // if (list === undefined) + // return undefined; + + // if (list[id] !== undefined) + // return id; + + // for (const [key, value] of Object.entries(list)) + // if (value.find(cluster => cluster.uuid === id)) + // return key; + + // return undefined; + // } + + // function move(fromId: string, toId: string) { + // const from = getContainerByCluster(fromId); + // const to = getContainerByCluster(toId); + + // console.log(from, to); + + // if (from === undefined || to === undefined) + // return; + + // if (from === to) { + // const list = clusters(); + // const fromList = list[from]; + // const fromIndex = fromList.findIndex(cluster => cluster.uuid === fromId); + // const toIndex = fromList.findIndex(cluster => cluster.uuid === toId); + + // if (fromIndex === toIndex) + // return; + + // const newList = [...fromList]; + // const [removed] = newList.splice(fromIndex, 1); + // newList.splice(toIndex, 0, removed); + + // mutate({ ...list, [from]: newList }); + // } + // else { + // const list = clusters(); + // const fromList = list[from]; + // const toList = list[to]; + // const fromIndex = fromList.findIndex(cluster => cluster.uuid === fromId); + // const toIndex = toList.findIndex(cluster => cluster.uuid === toId); + + // const newListFrom = [...fromList]; + // const newListTo = [...toList]; + + // const [removed] = newListFrom.splice(fromIndex, 1); + // newListTo.splice(toIndex, 0, removed); + + // mutate({ ...list, [from]: newListFrom, [to]: newListTo }); + // } + // } + + // const onDragEnd: DragEventHandler = (event) => { + // if (event.draggable && event.droppable) + // move(event.draggable.id as string, event.droppable.id as string); + // }; + return (
- +
@@ -54,17 +132,32 @@ function HomePage() {
- - + 0} + children={( + // + // + + + {([group, clusters]) => ( + + )} + + + // + // {draggable => ( + // draggable?.node.cloneNode(true) + // )} + // + // + )} + fallback={(
No clusters were found. Create one now with the New Cluster button.
-
- - - -
+ )} + />
); } @@ -89,7 +182,11 @@ function OneConfigLogo() { ); } -function Banner() { +interface BannerProps { + clusters: GroupedClusters | undefined; +}; + +function Banner(props: BannerProps) { // TODO: Banner information /** * If there are any clusters, display the most recent cluster name + some statistics as the "description". @@ -99,29 +196,89 @@ function Banner() { * * If there are no clusters, display a generic background with the button action creating a new cluster. */ + const [cluster, setCluster] = createSignal(); + const navigate = useNavigate(); + + createEffect(() => { + // eslint-disable-next-line no-undef-init -- This is fine. I love eslint rule clash though + let mostRecentCluster: Cluster | undefined = undefined; + + if (props.clusters === undefined) + return; + + for (const [_, clusters] of Object.entries(props.clusters)) + for (const cluster of clusters) { + if (mostRecentCluster === undefined) { + mostRecentCluster = cluster; + continue; + } + + if (typeof mostRecentCluster.meta.played_at !== 'string' && typeof cluster.meta.played_at === 'string') { + mostRecentCluster = cluster; + continue; + } + + if (typeof mostRecentCluster.meta.played_at === 'string' && typeof cluster.meta.played_at === 'string') { + const playedAt = new Date(mostRecentCluster.meta.played_at); + const clusterPlayedAt = new Date(cluster.meta.played_at); + + if (clusterPlayedAt > playedAt) + mostRecentCluster = cluster; + } + } + + setCluster(mostRecentCluster); + }); + return (
-

Building worlds

-

DEV BUILD - NOT FINAL

+

{cluster()?.meta.name || 'Create a cluster'}

+ +

+ You've played + {' '} + + {cluster()!.meta.mc_version} + {' '} + {upperFirst(cluster()!.meta.loader) || 'Unknown'} + + {' '} + for + {' '} + {secondsToWords((cluster()!.meta.overall_played || 0n))} + . +

+
-
- } /> - }>Verified + {/* TODO: These tags */} + {/* } /> + }>Verified */}
@@ -139,10 +296,6 @@ interface ClusterCardContextMenuProps { function ClusterCardContextMenu(props: ClusterCardContextMenuProps) { const navigate = useNavigate(); - function viewCluster() { - navigate(`/clusters/?id=${props.id}`); - } - return ( } text="View" - onClick={() => viewCluster()} + onClick={() => ClusterRoot.open(navigate, props.id)} /> @@ -170,6 +323,7 @@ function ClusterCard(props: Cluster) { const navigate = useNavigate(); const [pos, setPos] = createSignal({ x: 0, y: 0 }); const [contextMenuVisible, setContextMenuVisible] = createSignal(false); + // const sortable = createSortable(props.uuid, props); let ref!: HTMLDivElement; function openClusterPage(_e: MouseEvent) { @@ -192,7 +346,10 @@ function ClusterCard(props: Cluster) { onClick={e => openClusterPage(e)} onMouseDown={e => onMouseDown(e)} onContextMenu={e => openContextMenu(e)} - ref={ref} + ref={mergeRefs( + ref, + // sortable + )} class="relative h-[152px] group flex flex-col rounded-xl bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed border border-gray-05" >
@@ -239,11 +396,19 @@ interface ClusterGroupProps { } function ClusterGroup(props: ClusterGroupProps) { + // const droppable = createDroppable(props.title); + // const ids = createMemo(() => props.clusters.map(cluster => cluster.uuid)); + return ( -
+

{props.title}

-
+
+ {/* */} {item => } + {/* */}
); diff --git a/apps/frontend/src/ui/pages/cluster/ClusterGame.tsx b/apps/frontend/src/ui/pages/cluster/ClusterGame.tsx new file mode 100644 index 00000000..fe54ad11 --- /dev/null +++ b/apps/frontend/src/ui/pages/cluster/ClusterGame.tsx @@ -0,0 +1,20 @@ +import ScrollableContainer from '~ui/components/ScrollableContainer'; +import Sidebar from '~ui/components/Sidebar'; +import useClusterContext from '~ui/hooks/useCluster'; + +function ClusterGame() { + const cluster = useClusterContext(); + + // TODO: Save cluster settings + + return ( + +

Game Running

+ +

TODO

+
+
+ ); +} + +export default ClusterGame; diff --git a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx index 366d0b1c..a01824cc 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx @@ -1,4 +1,4 @@ -import { Route, useSearchParams } from '@solidjs/router'; +import { type Navigator, Route, useSearchParams } from '@solidjs/router'; import type { ParentProps } from 'solid-js'; import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-solid'; import Sidebar from '../../components/Sidebar'; @@ -9,6 +9,7 @@ import ClusterLogs from './ClusterLogs'; import ClusterMods from './ClusterMods'; import ClusterScreenshots from './ClusterScreenshots'; import ClusterSettings from './ClusterSettings'; +import ClusterGame from './ClusterGame'; import useClusterContext, { ClusterProvider } from '~ui/hooks/useCluster'; function ClusterRoutes() { @@ -19,6 +20,7 @@ function ClusterRoutes() { + ); } @@ -45,6 +47,15 @@ function ClusterRoot(props: ParentProps) { ); } +ClusterRoot.open = function (navigate: Navigator, uuid: string) { + navigate(`/clusters/?id=${uuid}`); +}; + +ClusterRoot.launch = function (navigate: Navigator, uuid: string) { + // TODO: Launch game page + navigate(`/clusters/game?id=${uuid}&launch=true`); +}; + function ClusterSidebar() { const [cluster] = useClusterContext(); @@ -60,6 +71,7 @@ function ClusterSidebar() { [, 'Worlds', '/worlds'], [, 'Logs', '/logs'], [, 'Game Settings', '/settings'], + // TODO: Add game page CONDITIONALLY ], }} /> diff --git a/apps/frontend/src/utils/primitives.ts b/apps/frontend/src/utils/primitives.ts index ed80e050..26bb416f 100644 --- a/apps/frontend/src/utils/primitives.ts +++ b/apps/frontend/src/utils/primitives.ts @@ -6,13 +6,13 @@ export function upperFirst(object: any): string { export function abbreviateNumber(n: number) { if (n < 1e3) return `${n}`; - if (n >= 1e3 && n < 1e6) + else if (n >= 1e3 && n < 1e6) return `${+(n / 1e3).toFixed(1)}K`; - if (n >= 1e6 && n < 1e9) + else if (n >= 1e6 && n < 1e9) return `${+(n / 1e6).toFixed(1)}M`; - if (n >= 1e9 && n < 1e12) + else if (n >= 1e9 && n < 1e12) return `${+(n / 1e9).toFixed(1)}B`; - if (n >= 1e12) + else if (n >= 1e12) return `${+(n / 1e12).toFixed(1)}T`; return `${n}`; }; @@ -23,6 +23,27 @@ export function getEnumMembers(obj: any): string[] { }); } +export function secondsToWords(seconds: number | bigint): string { + const n = Number(seconds); + + if (n < 60) + return `${n} seconds`; + else if (n < (60 * 60)) + return `${Math.floor(n / 60)} minutes`; + else if (n < (60 * 60 * 24)) + return `${Math.floor(n / (60 * 60))} hours`; + else if (n < (60 * 60 * 24 * 7)) + return `${Math.floor(n / (60 * 60 * 24))} days`; + else if (n < (60 * 60 * 24 * 30)) + return `${Math.floor(n / (60 * 60 * 24 * 7))} weeks`; + else if (n < (60 * 60 * 24 * 365)) + return `${Math.floor(n / (60 * 60 * 24 * 30))} months`; + else if (n < (60 * 60 * 24 * 365 * 10)) + return `${Math.floor(n / (60 * 60 * 24 * 365))} years`; + + return `${n} seconds`; // Hmmmmmm +} + export default { upperFirst, abbreviateNumber, diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index 4640a9d1..9c4af75f 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -60,6 +60,25 @@ pub async fn list(clear: Option) -> crate::Result> { .collect()) } +/// get a Map of [`Cluster`] group names containing lists of [`Cluster`]s +#[tracing::instrument] +pub async fn list_grouped(clear: Option) -> crate::Result>> { + // TODO: This can 100% be made better + let list = list(clear).await?; + let mut map = HashMap::>::new(); + + for cluster in list { + let group = cluster.meta.group.clone().unwrap_or(String::from("Ungrouped")); + if let Some(items) = map.get_mut(&group) { + items.push(cluster); + } else { + map.insert(group, vec![cluster]); + } + } + + Ok(map) +} + /// run a Minecraft [`Cluster`] using the default credentials. #[tracing::instrument] pub async fn run(path: &ClusterPath) -> crate::Result>> { diff --git a/packages/core/src/store/clusters.rs b/packages/core/src/store/clusters.rs index 45275f29..7c333969 100644 --- a/packages/core/src/store/clusters.rs +++ b/packages/core/src/store/clusters.rs @@ -257,7 +257,7 @@ pub struct ClusterMeta { /// (Defaults to None if the cluster has never been played) #[serde(skip_serializing_if = "Option::is_none")] pub played_at: Option>, - /// The overall time played stored as a [`u64`]. + /// The overall time played in seconds stored as a [`u64`]. #[serde(default)] pub overall_played: u64, /// The recent time played stored as a [`u64`]. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7514df..18e6512d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,13 +95,13 @@ importers: version: 2.0.0-beta.16 '@tauri-apps/plugin-clipboard-manager': specifier: github:tauri-apps/tauri-plugin-clipboard-manager#v2 - version: https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/d503abbce40595d0af6b36c10439b85c640b4c53 + version: https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/20e84cfbdebcd86bb1854846feb0aaff4481fbd0 '@tauri-apps/plugin-dialog': specifier: 2.0.0-beta.8 version: 2.0.0-beta.8 '@tauri-apps/plugin-shell': specifier: github:tauri-apps/tauri-plugin-shell#v2 - version: https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/adb938ddf434547ae35f74dc1a080908da03ea96 + version: https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/5cc36dadeaad0520111c6f5e32ecdbbae7cb1311 '@thisbeyond/solid-dnd': specifier: ^0.7.5 version: 0.7.5(solid-js@1.8.19) @@ -1268,16 +1268,16 @@ packages: engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-clipboard-manager@https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/d503abbce40595d0af6b36c10439b85c640b4c53': - resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/d503abbce40595d0af6b36c10439b85c640b4c53} - version: 2.1.0-beta.5 + '@tauri-apps/plugin-clipboard-manager@https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/20e84cfbdebcd86bb1854846feb0aaff4481fbd0': + resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/20e84cfbdebcd86bb1854846feb0aaff4481fbd0} + version: 2.1.0-beta.6 '@tauri-apps/plugin-dialog@2.0.0-beta.8': resolution: {integrity: sha512-6iVhEBceWYLUgX6ygBc4/yJ1o/D4YwgoAGt7ebFUTjEyIhp79PeFtE8OTjdyJbtVOc6bAcDeAn4+pbWtSw9/4w==} - '@tauri-apps/plugin-shell@https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/adb938ddf434547ae35f74dc1a080908da03ea96': - resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/adb938ddf434547ae35f74dc1a080908da03ea96} - version: 2.0.0-beta.8 + '@tauri-apps/plugin-shell@https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/5cc36dadeaad0520111c6f5e32ecdbbae7cb1311': + resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/5cc36dadeaad0520111c6f5e32ecdbbae7cb1311} + version: 2.0.0-beta.9 '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -4565,17 +4565,17 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.23 '@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.23 - '@tauri-apps/plugin-clipboard-manager@https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/d503abbce40595d0af6b36c10439b85c640b4c53': + '@tauri-apps/plugin-clipboard-manager@https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/20e84cfbdebcd86bb1854846feb0aaff4481fbd0': dependencies: - '@tauri-apps/api': 2.0.0-beta.15 + '@tauri-apps/api': 2.0.0-beta.16 '@tauri-apps/plugin-dialog@2.0.0-beta.8': dependencies: '@tauri-apps/api': 2.0.0-beta.16 - '@tauri-apps/plugin-shell@https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/adb938ddf434547ae35f74dc1a080908da03ea96': + '@tauri-apps/plugin-shell@https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/5cc36dadeaad0520111c6f5e32ecdbbae7cb1311': dependencies: - '@tauri-apps/api': 2.0.0-beta.15 + '@tauri-apps/api': 2.0.0-beta.16 '@testing-library/dom@10.4.0': dependencies: