From 32d27bc9fbccf37842deeb1840d7c871b6a448c0 Mon Sep 17 00:00:00 2001
From: LynithDev <61880709+LynithDev@users.noreply.github.com>
Date: Fri, 2 Aug 2024 12:30:27 +0200
Subject: [PATCH] feat: Revamp Cluster overview page, make it less empty
---
apps/frontend/src/globals.css | 15 +-
.../src/ui/components/SettingsRow.tsx | 11 +-
.../src/ui/components/base/Button.module.scss | 19 +-
apps/frontend/src/ui/pages/Home.tsx | 75 +---
.../src/ui/pages/cluster/ClusterOverview.tsx | 388 +++++++++++-------
.../settings/launcher/SettingsGeneral.tsx | 2 +-
apps/frontend/src/utils/primitives.ts | 49 ++-
7 files changed, 317 insertions(+), 242 deletions(-)
diff --git a/apps/frontend/src/globals.css b/apps/frontend/src/globals.css
index fbc756a6..adb6ef8b 100644
--- a/apps/frontend/src/globals.css
+++ b/apps/frontend/src/globals.css
@@ -3,17 +3,24 @@ body {
}
/* start lynith's native experience (tm) */
-* {
+* {
-webkit-user-select: none;
user-select: none;
-webkit-user-drag: none;
user-drag: none;
- cursor: default !important;
+ cursor: default !important;
animation-duration: 150ms !important;
}
-*:hover {
- cursor: default !important;
+*:hover {
+ cursor: default !important;
+}
+
+img {
+ user-select: none;
+ -webkit-user-drag: none;
+ user-drag: none;
+ pointer-events: none;
}
/* end lynith's native experience (tm) */
diff --git a/apps/frontend/src/ui/components/SettingsRow.tsx b/apps/frontend/src/ui/components/SettingsRow.tsx
index 1062d4ff..cbc4f58c 100644
--- a/apps/frontend/src/ui/components/SettingsRow.tsx
+++ b/apps/frontend/src/ui/components/SettingsRow.tsx
@@ -4,14 +4,17 @@ type SettingsRowProps = ParentProps & {
title: JSX.Element;
description: JSX.Element;
icon: JSX.Element;
- onClick?: () => any;
+ disabled?: boolean;
};
function SettingsRow(props: SettingsRowProps) {
return (
{props.icon}
@@ -22,7 +25,7 @@ function SettingsRow(props: SettingsRowProps) {
{props.description}
-
diff --git a/apps/frontend/src/ui/components/base/Button.module.scss b/apps/frontend/src/ui/components/base/Button.module.scss
index 7344d5da..7be37c94 100644
--- a/apps/frontend/src/ui/components/base/Button.module.scss
+++ b/apps/frontend/src/ui/components/base/Button.module.scss
@@ -1,26 +1,19 @@
.button {
@apply py-1.5 px-3 rounded-lg flex flex-row justify-center items-center gap-x-2.5;
- &:disabled {
- @apply border-op-1! bg-opacity-10! text-op-10! pointer-events-none;
-
- svg {
- @apply fill-op-10! stroke-op-10!;
- }
- }
-
&__primary {
@apply text-onbrand hover:text-onbrand-hover active:text-onbrand-pressed disabled:text-onbrand-disabled;
@apply bg-brand hover:bg-brand-hover active:bg-brand-pressed disabled:bg-brand-disabled;
}
&__secondary, &__iconSecondary {
- @apply border border-gray-05;
+ @apply border box-border border-gray-05;
@apply text-fg-primary hover:text-fg-primary-hover active:text-fg-primary-pressed disabled:text-fg-primary-disabled;
@apply bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed disabled:bg-component-bg-disabled;
}
&__danger, &__iconDanger {
+ @apply text-onbrand hover:text-onbrand-hover active:text-onbrand-pressed disabled:text-onbrand-disabled;
@apply bg-danger hover:bg-danger-hover active:bg-danger-pressed disabled:bg-danger-disabled;
}
@@ -62,6 +55,14 @@
@apply stroke-fg-primary-disabled;
}
}
+
+ &:disabled {
+ @apply border-opacity-1! bg-opacity-10! text-opacity-10! pointer-events-none;
+
+ svg {
+ @apply fill-opacity-10! stroke-opacity-10!;
+ }
+ }
}
.icon > * {
diff --git a/apps/frontend/src/ui/pages/Home.tsx b/apps/frontend/src/ui/pages/Home.tsx
index 2596bc06..35a1404d 100644
--- a/apps/frontend/src/ui/pages/Home.tsx
+++ b/apps/frontend/src/ui/pages/Home.tsx
@@ -1,18 +1,14 @@
import {
ChevronRightIcon,
- DotsVerticalIcon,
- EyeIcon,
PlayIcon,
PlusIcon,
SearchMdIcon,
} from '@untitled-theme/icons-solid';
-import type { Accessor, Setter } from 'solid-js';
import { For, Show, createEffect, createSignal } from 'solid-js';
import { useNavigate } from '@solidjs/router';
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 TextField from '../components/base/TextField';
import ClusterRoot from './cluster/ClusterRoot';
@@ -276,43 +272,8 @@ function Banner(props: BannerProps) {
);
}
-interface ClusterCardContextMenuProps {
- pos: Accessor<{ x: number; y: number }>;
- contextMenuVisible: Accessor
;
- setContextMenuVisible: Setter;
- id: string;
-}
-
-function ClusterCardContextMenu(props: ClusterCardContextMenuProps) {
- const navigate = useNavigate();
-
- return (
-
- }
- text="View"
- onClick={() => ClusterRoot.open(navigate, props.id)}
- />
-
-
-
- }
- text="Launch"
- onClick={() => {}}
- />
-
- );
-}
-
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;
@@ -320,22 +281,10 @@ function ClusterCard(props: Cluster) {
navigate(`/clusters/?id=${props.uuid}`);
}
- function openContextMenu(e: MouseEvent) {
- e.preventDefault();
- e.stopPropagation();
- setContextMenuVisible(!contextMenuVisible());
- }
-
- function onMouseDown(e: MouseEvent) {
- setPos({ x: e.clientX, y: e.clientY });
- }
-
return (
<>
openClusterPage(e)}
- onMouseDown={e => onMouseDown(e)}
- onContextMenu={e => openContextMenu(e)}
ref={mergeRefs(
ref,
// sortable
@@ -353,9 +302,9 @@ function ClusterCard(props: Cluster) {
/>
-
-
-
{props.meta.name}
+
+
+
{props.meta.name}
{upperFirst(props.meta.loader)}
{' '}
@@ -364,18 +313,18 @@ function ClusterCard(props: Cluster) {
{props.instance.mods && `• ${props.mods} mods`} */}
-
-
-
>
);
}
diff --git a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx
index d8e1f4ba..af8efe0e 100644
--- a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx
+++ b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx
@@ -1,35 +1,184 @@
-import { Edit02Icon, ImagePlusIcon, PlayIcon, Save01Icon, Share07Icon, Trash01Icon } from '@untitled-theme/icons-solid';
-import { Show, createSignal, untrack } from 'solid-js';
+import { Edit02Icon, FolderIcon, ImagePlusIcon, LinkExternal01Icon, PlayIcon, Save01Icon, Share07Icon, Trash01Icon } from '@untitled-theme/icons-solid';
+import { type Accessor, type Setter, Show, createSignal, untrack } from 'solid-js';
import { useBeforeLeave, useNavigate } from '@solidjs/router';
import * as dialog from '@tauri-apps/plugin-dialog';
+import { open } from '@tauri-apps/plugin-shell';
import ClusterCover from '../../components/game/ClusterCover';
import LoaderIcon from '../../components/game/LoaderIcon';
import Button from '../../components/base/Button';
+import ClusterRoot from './ClusterRoot';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import type { Cluster } from '~bindings';
-import { upperFirst } from '~utils/primitives';
+import { secondsToWords, upperFirst } from '~utils/primitives';
import useClusterContext from '~ui/hooks/useCluster';
import Modal from '~ui/components/overlay/Modal';
import { bridge } from '~imports';
-import { tryResult } from '~ui/hooks/useCommand';
import TextField from '~ui/components/base/TextField';
+import SettingsRow from '~ui/components/SettingsRow';
+import useSettingsContext from '~ui/hooks/useSettings';
+import joinPath from '~utils/helpers';
function ClusterOverview() {
+ const settings = useSettingsContext();
const [cluster, { refetch }] = useClusterContext();
const [deleteVisible, setDeleteVisible] = createSignal(false);
+ const [saveVisible, setSaveVisible] = createSignal(false);
+
+ const [editMode, setEditMode] = createSignal(false);
+ const [newName, setNewName] = createSignal('');
+ const [newCover, setNewCover] = createSignal('');
+
const navigate = useNavigate();
+ useBeforeLeave((e) => {
+ if (editMode() && madeChanges()) {
+ e.preventDefault();
+ setSaveVisible(true);
+ }
+ });
+
+ function getPath() {
+ const clusterPath = cluster()?.path;
+ const configDir = settings.config_dir;
+
+ if (typeof clusterPath !== 'string' || typeof configDir !== 'string')
+ return '';
+
+ return joinPath(configDir, 'clusters', clusterPath);
+ }
+
+ function openPath() {
+ open(getPath());
+ }
+
async function deleteCluster() {
await bridge.commands.removeCluster(cluster()!.uuid);
navigate('/');
}
+ function madeChanges() {
+ const name = untrack(() => newName());
+ const cover = untrack(() => newCover());
+ const c = untrack(() => cluster());
+
+ if (!c)
+ return false;
+
+ const changedName = (name.length > 0 && name !== c.meta.name);
+ const changedCover = (cover.length > 0 && (cover !== c.meta.icon_url || cover !== c.meta.icon));
+
+ return changedName || changedCover;
+ }
+
+ function toggleEditMode() {
+ const next = !untrack(() => editMode());
+
+ if (next === false && madeChanges()) {
+ setSaveVisible(true);
+ return;
+ }
+
+ setEditMode(next);
+ }
+
+ async function pushEdits() {
+ const c = untrack(() => cluster());
+
+ if (!c)
+ return;
+
+ await bridge.commands.editCluster(
+ c.uuid,
+ newName().length > 0 ? newName() : null,
+ newCover().length > 0 ? newCover() : c.meta.icon_url || c.meta.icon || null,
+ );
+
+ refetch();
+ }
+
+ function dontSave() {
+ setSaveVisible(false);
+ setEditMode(false);
+ }
+
+ function save() {
+ pushEdits();
+ setSaveVisible(false);
+ setEditMode(false);
+ }
+
return (
Overview
- setDeleteVisible(true)} />
+
+
+ Folders and Files
+ }
+ disabled={editMode()}
+ children={(
+ }
+ onClick={openPath}
+ disabled={editMode()}
+ />
+ )}
+ />
+
+ Cluster Actions
+ }
+ children={(
+ }
+ children={}
+ />
+ )}
+ children={editMode() ? 'Save' : 'Edit'}
+ checked={editMode}
+ onChecked={toggleEditMode}
+ />
+ )}
+ />
+ }
+ disabled={editMode()}
+ children={(
+ }
+ onClick={() => setDeleteVisible(true)}
+ disabled={editMode()}
+ />
+ )}
+ />
+
+ setSaveVisible(false)}
+ />,
+ ,
+ ,
+ ]}
+ />
);
}
interface BannerProps {
cluster: Cluster;
+ saveVisible: Accessor
;
+ setSaveVisible: Setter;
+ editMode: Accessor;
+ newName: Accessor;
+ setNewName: Setter;
+ newCover: Accessor;
+ setNewCover: Setter;
refetch: () => void;
- showDeletePrompt: () => void;
}
function Banner(props: BannerProps) {
- const [editMode, setEditMode] = createSignal(false);
- const [newName, setNewName] = createSignal('');
- const [newCover, setNewCover] = createSignal('');
-
- const [modalVisible, setModalVisible] = createSignal(false);
+ const navigate = useNavigate();
async function launch() {
- const [_uuid, _pid] = await tryResult(bridge.commands.runCluster, props.cluster.uuid);
+ ClusterRoot.launch(navigate, props.cluster.uuid);
}
- useBeforeLeave((e) => {
- if (editMode() && madeChanges()) {
- e.preventDefault();
- setModalVisible(true);
- }
- });
-
async function launchFilePicker() {
const selected = await dialog.open({
multiple: false,
@@ -78,62 +246,20 @@ function Banner(props: BannerProps) {
if (selected === null)
return;
- setNewCover(selected.path);
+ props.setNewCover(selected.path);
}
function updateName(name: string) {
if (name.length > 30 || name.length <= 0)
return;
- setNewName(name);
- }
-
- async function pushEdits() {
- await bridge.commands.editCluster(
- props.cluster.uuid,
- newName().length > 0 ? newName() : null,
- newCover().length > 0 ? newCover() : props.cluster.meta.icon_url || props.cluster.meta.icon || null,
- );
-
- props.refetch();
- }
-
- function madeChanges() {
- const name = untrack(() => newName());
- const cover = untrack(() => newCover());
-
- const changedName = (name.length > 0 && name !== props.cluster.meta.name);
- const changedCover = (cover.length > 0 && (cover !== props.cluster.meta.icon_url || cover !== props.cluster.meta.icon));
-
- return changedName || changedCover;
- }
-
- async function toggleEditMode() {
- const next = !untrack(() => editMode());
-
- if (next === false && madeChanges()) {
- setModalVisible(true);
- return;
- }
-
- setEditMode(next);
- }
-
- function dontSave() {
- setModalVisible(false);
- setEditMode(false);
- }
-
- function save() {
- pushEdits();
- setModalVisible(false);
- setEditMode(false);
+ props.setNewName(name);
}
return (
-
-
+
+
-
+
-
+
{props.cluster.meta.name}
+ {props.cluster.meta.name}
}
>
updateName(e.target.value)}
/>
-
-
-
-
- {upperFirst(props.cluster.meta.loader || 'unknown')}
- {props.cluster.meta.loader_version && {props.cluster.meta.loader_version.id}}
- {props.cluster.meta.mc_version}
-
-
- Played for
- {' '}
- {((props.cluster.meta.overall_played || 0 as unknown as bigint)).toString()}
- {' '}
- hours
-
-
+
+
+
+
+ {upperFirst(props.cluster.meta.loader || 'unknown')}
+ {props.cluster.meta.loader_version && {props.cluster.meta.loader_version.id}}
+ {props.cluster.meta.mc_version}
+
+
+ Played for
+ {' '}
+ {secondsToWords((props.cluster.meta.overall_played || 0n))}
+ .
+
+
-
+
-
-
- }
- disabled={editMode()}
- />
-
- }
- onClick={props.showDeletePrompt}
- disabled={editMode()}
- />
-
- }
- children="Launch"
- class="!w-auto"
- onClick={launch}
- disabled={editMode()}
- />
-
- }
- children={}
+ }
+ disabled={props.editMode()}
/>
- )}
- checked={editMode}
- onChecked={toggleEditMode}
- />
-
-
setModalVisible(false)}
- />,
- ,
- ,
- ]}
- />
+ }
+ children="Launch"
+ class="!w-auto"
+ onClick={launch}
+ disabled={props.editMode()}
+ />
+
+
+
+
);
}
diff --git a/apps/frontend/src/ui/pages/settings/launcher/SettingsGeneral.tsx b/apps/frontend/src/ui/pages/settings/launcher/SettingsGeneral.tsx
index a4f1944b..d7bed38d 100644
--- a/apps/frontend/src/ui/pages/settings/launcher/SettingsGeneral.tsx
+++ b/apps/frontend/src/ui/pages/settings/launcher/SettingsGeneral.tsx
@@ -52,7 +52,7 @@ function SettingsGeneral() {
Folders and Files
}
>
diff --git a/apps/frontend/src/utils/primitives.ts b/apps/frontend/src/utils/primitives.ts
index 26bb416f..52a94dc9 100644
--- a/apps/frontend/src/utils/primitives.ts
+++ b/apps/frontend/src/utils/primitives.ts
@@ -23,25 +23,42 @@ export function getEnumMembers(obj: any): string[] {
});
}
-export function secondsToWords(seconds: number | bigint): string {
+// pro lynith move
+// TODO: Probably should use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules
+export function pluralize(n: number, word: string): string {
+ return n === 1 ? word : `${word}s`;
+}
+
+export function secondsToWords(seconds: number | bigint, plural: boolean = true): 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`;
+ const func = (n: number) => {
+ if (n < 60)
+ return `${n} second`;
+ else if (n < (60 * 60))
+ return `${Math.floor(n / 60)} minute`;
+ else if (n < (60 * 60 * 24))
+ return `${Math.floor(n / (60 * 60))} hour`;
+ else if (n < (60 * 60 * 24 * 7))
+ return `${Math.floor(n / (60 * 60 * 24))} day`;
+ else if (n < (60 * 60 * 24 * 30))
+ return `${Math.floor(n / (60 * 60 * 24 * 7))} week`;
+ else if (n < (60 * 60 * 24 * 365))
+ return `${Math.floor(n / (60 * 60 * 24 * 30))} month`;
+ else if (n < (60 * 60 * 24 * 365 * 10))
+ return `${Math.floor(n / (60 * 60 * 24 * 365))} year`;
- return `${n} seconds`; // Hmmmmmm
+ return `${n} second`; // Hmmmmmm
+ };
+
+ const str = func(n);
+ if (plural) {
+ const n = str.split(' ')[0];
+ const word = str.split(' ')[1];
+ return `${n} ${pluralize(Number(n), word as string)}`;
+ }
+
+ return str;
}
export default {