chore: Start work on drag n drop

This commit is contained in:
LynithDev 2024-08-01 17:48:20 +02:00
parent 2ec1a04c8b
commit 5dcc24e682
5 changed files with 151 additions and 27 deletions

View file

@ -1,5 +1,4 @@
use std::path::PathBuf; use std::{collections::HashMap, path::PathBuf, str::FromStr};
use std::str::FromStr;
use onelauncher::cluster::content::logger; use onelauncher::cluster::content::logger;
use onelauncher::cluster::{self}; use onelauncher::cluster::{self};
@ -25,6 +24,12 @@ pub async fn get_clusters() -> Result<Vec<Cluster>, String> {
Ok(onelauncher::cluster::list(None).await?) 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)] #[derive(Serialize, Deserialize, Type)]
pub struct CreateCluster { pub struct CreateCluster {
name: String, name: String,

View file

@ -33,6 +33,7 @@ macro_rules! collect_commands {
remove_cluster, remove_cluster,
get_cluster, get_cluster,
get_clusters, get_clusters,
get_clusters_grouped,
run_cluster, run_cluster,
get_cluster_logs, get_cluster_logs,
get_cluster_log, get_cluster_log,

View file

@ -7,8 +7,21 @@ import {
SearchMdIcon, SearchMdIcon,
} from '@untitled-theme/icons-solid'; } from '@untitled-theme/icons-solid';
import type { Accessor, Setter } from 'solid-js'; import type { Accessor, Setter } from 'solid-js';
import { For, Match, Switch, createSignal } from 'solid-js'; import { For, Show, batch, createMemo, createSignal, untrack } from 'solid-js';
import { useNavigate } from '@solidjs/router'; import { useNavigate } from '@solidjs/router';
import {
DragDropProvider,
DragDropSensors,
type DragEventHandler,
DragOverlay,
type Draggable,
type Droppable,
SortableProvider,
closestCenter,
createDroppable,
createSortable,
} from '@thisbeyond/solid-dnd';
import { mergeRefs } from '@solid-primitives/refs';
import image from '../../assets/images/header.png'; import image from '../../assets/images/header.png';
import ContextMenu from '../components/overlay/ContextMenu'; import ContextMenu from '../components/overlay/ContextMenu';
import Button from '../components/base/Button'; import Button from '../components/base/Button';
@ -16,15 +29,42 @@ import Tag from '../components/base/Tag';
import TextField from '../components/base/TextField'; import TextField from '../components/base/TextField';
import ClusterCover from '~ui/components/game/ClusterCover'; import ClusterCover from '~ui/components/game/ClusterCover';
import type { Cluster } from '~bindings'; import type { Cluster } from '~bindings';
import useCommand from '~ui/hooks/useCommand'; import useCommand, { tryResult } from '~ui/hooks/useCommand';
import { bridge } from '~imports'; import { bridge } from '~imports';
import { upperFirst } from '~utils/primitives'; import { upperFirst } from '~utils/primitives';
import { useClusterModalController } from '~ui/components/overlay/cluster/ClusterCreationModal'; import { useClusterModalController } from '~ui/components/overlay/cluster/ClusterCreationModal';
function HomePage() { function HomePage() {
const [clusters, { refetch }] = useCommand(bridge.commands.getClusters); const [clusters, { refetch, mutate }] = useCommand(bridge.commands.getClustersGrouped);
const controller = useClusterModalController(); const controller = useClusterModalController();
function setClusters(group: string, clusters: Cluster[]) {
mutate((prev) => {
if (prev === undefined)
prev = {};
prev[group] = clusters;
return prev;
});
}
const containerIds = () => Object.keys(clusters);
const isContainer = (id: string) => containerIds().includes(id);
function getContainer(id: string) {
const list = untrack(() => clusters());
if (list === undefined)
return undefined;
for (const [key, items] of Object.entries(list))
if (items.find(cluster => cluster.uuid === id))
return key;
return undefined;
};
async function newCluster() { async function newCluster() {
try { try {
await controller.start(); await controller.start();
@ -36,6 +76,44 @@ function HomePage() {
refetch(); refetch();
} }
const move = (draggable: Draggable, droppable: Droppable, onlyWhenChangingContainer: boolean) => {
// console.log('Draggable', draggable, 'Droppable', droppable);
const draggableContainer = getContainer(draggable.id);
const droppableContainer = isContainer(droppable.id)
? droppable.id
: getContainer(droppable.id);
if (
draggableContainer != droppableContainer
|| !onlyWhenChangingContainer
) {
const containerItemIds = clusters()![droppableContainer];
let index = containerItemIds.indexOf(droppable.id);
if (index === -1)
index = containerItemIds.length;
batch(() => {
setClusters(draggableContainer, items =>
items.filter(item => item !== draggable.id));
setClusters(droppableContainer, items => [
...items.slice(0, index),
draggable.id,
...items.slice(index),
]);
});
}
};
const onDragEnd: DragEventHandler = (event) => {
if (event.draggable && event.droppable)
move(event.draggable, event.droppable, false);
};
const onDragOver: DragEventHandler = (event) => {
if (event.draggable && event.droppable)
move(event.draggable, event.droppable, true);
};
return ( return (
<div class="flex flex-col h-full gap-y-4 text-fg-primary"> <div class="flex flex-col h-full gap-y-4 text-fg-primary">
<Banner /> <Banner />
@ -54,17 +132,32 @@ function HomePage() {
</div> </div>
</div> </div>
<Switch> <DragDropProvider
<Match when={clusters()?.length === 0}> onDragOver={onDragOver}
onDragEnd={onDragEnd}
>
<DragDropSensors />
<Show
when={Object.keys(clusters() ?? {}).length === 0}
fallback={(
<For each={Object.entries(clusters() ?? {})}>
{([group, clusters]) => (
<ClusterGroup title={group} clusters={clusters} />
)}
</For>
)}
>
<div class="flex flex-col flex-1 gap-y-4 max-h-64 justify-center items-center"> <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-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> <span class="text-xl font-bold">Create one now with the New Cluster button.</span>
</div> </div>
</Match> </Show>
<Match when={clusters() !== undefined}> <DragOverlay>
<ClusterGroup title="Clusters" clusters={clusters()!} /> {draggable => (
</Match> draggable?.node.cloneNode(true)
</Switch> )}
</DragOverlay>
</DragDropProvider>
</div> </div>
); );
} }
@ -170,6 +263,7 @@ function ClusterCard(props: Cluster) {
const navigate = useNavigate(); const navigate = useNavigate();
const [pos, setPos] = createSignal({ x: 0, y: 0 }); const [pos, setPos] = createSignal({ x: 0, y: 0 });
const [contextMenuVisible, setContextMenuVisible] = createSignal(false); const [contextMenuVisible, setContextMenuVisible] = createSignal(false);
const sortable = createSortable(props.uuid);
let ref!: HTMLDivElement; let ref!: HTMLDivElement;
function openClusterPage(_e: MouseEvent) { function openClusterPage(_e: MouseEvent) {
@ -192,7 +286,7 @@ function ClusterCard(props: Cluster) {
onClick={e => openClusterPage(e)} onClick={e => openClusterPage(e)}
onMouseDown={e => onMouseDown(e)} onMouseDown={e => onMouseDown(e)}
onContextMenu={e => openContextMenu(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" 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"> <div class="flex-1 relative overflow-hidden rounded-t-xl">
@ -239,11 +333,16 @@ interface ClusterGroupProps {
} }
function ClusterGroup(props: ClusterGroupProps) { function ClusterGroup(props: ClusterGroupProps) {
const droppable = createDroppable(props.title);
const ids = createMemo(() => props.clusters.map(cluster => cluster.uuid));
return ( return (
<div class="flex flex-col gap-y-4"> <div ref={droppable} class="flex flex-col gap-y-4">
<h4>{props.title}</h4> <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">
<For each={props.clusters}>{item => <ClusterCard {...item} />}</For> <SortableProvider ids={ids()}>
<For each={props.clusters}>{item => <ClusterCard {...item} />}</For>
</SortableProvider>
</div> </div>
</div> </div>
); );

View file

@ -60,6 +60,25 @@ pub async fn list(clear: Option<bool>) -> crate::Result<Vec<Cluster>> {
.collect()) .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. /// run a Minecraft [`Cluster`] using the default credentials.
#[tracing::instrument] #[tracing::instrument]
pub async fn run(path: &ClusterPath) -> crate::Result<Arc<RwLock<ProcessorChild>>> { pub async fn run(path: &ClusterPath) -> crate::Result<Arc<RwLock<ProcessorChild>>> {

24
pnpm-lock.yaml generated
View file

@ -95,13 +95,13 @@ importers:
version: 2.0.0-beta.16 version: 2.0.0-beta.16
'@tauri-apps/plugin-clipboard-manager': '@tauri-apps/plugin-clipboard-manager':
specifier: github:tauri-apps/tauri-plugin-clipboard-manager#v2 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': '@tauri-apps/plugin-dialog':
specifier: 2.0.0-beta.8 specifier: 2.0.0-beta.8
version: 2.0.0-beta.8 version: 2.0.0-beta.8
'@tauri-apps/plugin-shell': '@tauri-apps/plugin-shell':
specifier: github:tauri-apps/tauri-plugin-shell#v2 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': '@thisbeyond/solid-dnd':
specifier: ^0.7.5 specifier: ^0.7.5
version: 0.7.5(solid-js@1.8.19) version: 0.7.5(solid-js@1.8.19)
@ -1268,16 +1268,16 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@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':
resolution: {tarball: 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/20e84cfbdebcd86bb1854846feb0aaff4481fbd0}
version: 2.1.0-beta.5 version: 2.1.0-beta.6
'@tauri-apps/plugin-dialog@2.0.0-beta.8': '@tauri-apps/plugin-dialog@2.0.0-beta.8':
resolution: {integrity: sha512-6iVhEBceWYLUgX6ygBc4/yJ1o/D4YwgoAGt7ebFUTjEyIhp79PeFtE8OTjdyJbtVOc6bAcDeAn4+pbWtSw9/4w==} resolution: {integrity: sha512-6iVhEBceWYLUgX6ygBc4/yJ1o/D4YwgoAGt7ebFUTjEyIhp79PeFtE8OTjdyJbtVOc6bAcDeAn4+pbWtSw9/4w==}
'@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':
resolution: {tarball: 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/5cc36dadeaad0520111c6f5e32ecdbbae7cb1311}
version: 2.0.0-beta.8 version: 2.0.0-beta.9
'@testing-library/dom@10.4.0': '@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} 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-ia32-msvc': 2.0.0-beta.23
'@tauri-apps/cli-win32-x64-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: 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': '@tauri-apps/plugin-dialog@2.0.0-beta.8':
dependencies: dependencies:
'@tauri-apps/api': 2.0.0-beta.16 '@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: dependencies:
'@tauri-apps/api': 2.0.0-beta.15 '@tauri-apps/api': 2.0.0-beta.16
'@testing-library/dom@10.4.0': '@testing-library/dom@10.4.0':
dependencies: dependencies: