Merge branch 'main' of github.com:Polyfrost/OneLauncher

This commit is contained in:
Pauline 2024-08-02 05:29:51 +02:00
commit d4781db671
No known key found for this signature in database
GPG key ID: 722BB181973296AE
10 changed files with 296 additions and 53 deletions

View file

@ -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<Vec<Cluster>, String> {
Ok(onelauncher::cluster::list(None).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_clusters_grouped() -> Result<HashMap<String, Vec<Cluster>>, String> {
Ok(onelauncher::cluster::list_grouped(None).await?)
}
#[derive(Serialize, Deserialize, Type)]
pub struct CreateCluster {
name: String,

View file

@ -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,

View file

@ -6,7 +6,7 @@ import type { Result } from '~bindings';
* @param cmd The command to call
* @throws
*/
export default function useCommand<R, E, Args extends unknown[]>(
export default function useCommand<R, E = string, Args extends unknown[] = []>(
cmd: (...args: Args) => Promise<Result<R, E>>,
...args: Args
): ResourceReturn<R> {

View file

@ -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<string, Cluster[]>;
function HomePage() {
const [clusters, { refetch }] = useCommand(bridge.commands.getClusters);
const [clusters, { refetch }] = useCommand<GroupedClusters>(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 (
<div class="flex flex-col h-full gap-y-4 text-fg-primary">
<Banner />
<Banner clusters={clusters()} />
<div class="flex flex-row justify-between items-center">
<div>
@ -54,17 +132,32 @@ function HomePage() {
</div>
</div>
<Switch>
<Match when={clusters()?.length === 0}>
<Show
when={containerIds(clusters()).length > 0}
children={(
// <DragDropProvider onDragEnd={onDragEnd}>
// <DragDropSensors />
<For each={Object.entries(clusters() ?? {})}>
{([group, clusters]) => (
<ClusterGroup title={group} clusters={clusters} />
)}
</For>
// <DragOverlay>
// {draggable => (
// draggable?.node.cloneNode(true)
// )}
// </DragOverlay>
// </DragDropProvider>
)}
fallback={(
<div class="flex flex-col flex-1 gap-y-4 max-h-64 justify-center items-center">
<span class="text-lg font-bold uppercase text-fg-secondary">No clusters were found.</span>
<span class="text-xl font-bold">Create one now with the New Cluster button.</span>
</div>
</Match>
<Match when={clusters() !== undefined}>
<ClusterGroup title="Clusters" clusters={clusters()!} />
</Match>
</Switch>
)}
/>
</div>
);
}
@ -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<Cluster>();
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 (
<div class="relative w-full h-52">
<img src={image} class="absolute rounded-xl w-full h-52 object-cover" />
<div class="relative z-10 h-full px-8 py-6 text-fg-primary flex flex-col justify-between items-start">
<div class="flex flex-col gap-y-2">
<h1>Building worlds</h1>
<p>DEV BUILD - NOT FINAL</p>
<h1>{cluster()?.meta.name || 'Create a cluster'}</h1>
<Show when={cluster() !== undefined}>
<p>
You've played
{' '}
<strong>
{cluster()!.meta.mc_version}
{' '}
{upperFirst(cluster()!.meta.loader) || 'Unknown'}
</strong>
{' '}
for
{' '}
<strong>{secondsToWords((cluster()!.meta.overall_played || 0n))}</strong>
.
</p>
</Show>
</div>
<div class="flex w-full flex-row justify-between items-end">
<div class="flex flex-row items-center gap-x-4">
<Button
buttonStyle="primary"
iconLeft={<PlayIcon />}
children="Launch 1.20.4"
/>
<Button
buttonStyle="icon"
children={<DotsVerticalIcon />}
<Show
when={cluster() !== undefined}
children={(
<>
<Button
buttonStyle="primary"
iconLeft={<PlayIcon />}
children={`Launch ${cluster()!.meta.mc_version}`}
onClick={() => ClusterRoot.launch(navigate, cluster()!.uuid)}
/>
<Button
buttonStyle="iconSecondary"
class="bg-op-10!"
children={<ChevronRightIcon />}
onClick={() => ClusterRoot.open(navigate, cluster()!.uuid)}
/>
</>
)}
/>
</div>
<div class="flex flex-row gap-x-2">
<Tag iconLeft={<OneConfigLogo />} />
<Tag iconLeft={<CheckIcon />}>Verified</Tag>
{/* TODO: These tags */}
{/* <Tag iconLeft={<OneConfigLogo />} />
<Tag iconLeft={<CheckIcon />}>Verified</Tag> */}
</div>
</div>
</div>
@ -139,10 +296,6 @@ interface ClusterCardContextMenuProps {
function ClusterCardContextMenu(props: ClusterCardContextMenuProps) {
const navigate = useNavigate();
function viewCluster() {
navigate(`/clusters/?id=${props.id}`);
}
return (
<ContextMenu
pos={props.pos}
@ -152,7 +305,7 @@ function ClusterCardContextMenu(props: ClusterCardContextMenuProps) {
<ContextMenu.Row
icon={<EyeIcon />}
text="View"
onClick={() => viewCluster()}
onClick={() => ClusterRoot.open(navigate, props.id)}
/>
<ContextMenu.Seperator />
@ -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"
>
<div class="flex-1 relative overflow-hidden rounded-t-xl">
@ -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 (
<div class="flex flex-col gap-y-4">
<div
// ref={droppable}
class="flex flex-col gap-y-4"
>
<h4>{props.title}</h4>
<div class="grid grid-cols-4 2xl:grid-cols-6 gap-4">
<div class="grid grid-cols-4 2xl:grid-cols-6 gap-4 min-h-38">
{/* <SortableProvider ids={ids()}> */}
<For each={props.clusters}>{item => <ClusterCard {...item} />}</For>
{/* </SortableProvider> */}
</div>
</div>
);

View file

@ -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 (
<Sidebar.Page>
<h1>Game Running</h1>
<ScrollableContainer>
<p>TODO</p>
</ScrollableContainer>
</Sidebar.Page>
);
}
export default ClusterGame;

View file

@ -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() {
<Route path="/mods" component={ClusterMods} />
<Route path="/screenshots" component={ClusterScreenshots} />
<Route path="/settings" component={ClusterSettings} />
<Route path="/game" component={ClusterGame} />
</>
);
}
@ -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() {
[<Globe04Icon />, 'Worlds', '/worlds'],
[<File06Icon />, 'Logs', '/logs'],
[<Settings04Icon />, 'Game Settings', '/settings'],
// TODO: Add game page CONDITIONALLY
],
}}
/>

View file

@ -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,

View file

@ -60,6 +60,25 @@ pub async fn list(clear: Option<bool>) -> crate::Result<Vec<Cluster>> {
.collect())
}
/// get a Map of [`Cluster`] group names containing lists of [`Cluster`]s
#[tracing::instrument]
pub async fn list_grouped(clear: Option<bool>) -> crate::Result<HashMap<String, Vec<Cluster>>> {
// TODO: This can 100% be made better
let list = list(clear).await?;
let mut map = HashMap::<String, Vec<Cluster>>::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<Arc<RwLock<ProcessorChild>>> {

View file

@ -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<DateTime<Utc>>,
/// 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`].

24
pnpm-lock.yaml generated
View file

@ -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: