feat: Cluster Worlds Page
This commit is contained in:
parent
dfb8edd069
commit
444061c0a7
11 changed files with 186 additions and 9 deletions
|
|
@ -81,6 +81,10 @@ pub async fn edit_game_settings(uuid: Uuid, new_cluster: Cluster) -> Result<(),
|
|||
old.memory = new_cluster.memory;
|
||||
|
||||
// Process
|
||||
old.init_hooks = new_cluster.init_hooks.clone();
|
||||
|
||||
// Java
|
||||
old.java = new_cluster.java.clone();
|
||||
|
||||
async move { Ok(()) }
|
||||
})
|
||||
|
|
@ -195,3 +199,16 @@ pub async fn get_screenshots(uuid: Uuid) -> Result<Vec<String>, String> {
|
|||
|
||||
Ok(screenshots)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_worlds(uuid: Uuid) -> Result<Vec<String>, String> {
|
||||
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
|
||||
.await?
|
||||
.ok_or("cluster not found")?;
|
||||
|
||||
let screenshots = cluster::content::worlds::get_worlds(&cluster.cluster_path())
|
||||
.await?;
|
||||
|
||||
Ok(screenshots)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ macro_rules! collect_commands {
|
|||
get_cluster_log,
|
||||
upload_log,
|
||||
get_screenshots,
|
||||
get_worlds,
|
||||
// Processor
|
||||
get_running_clusters,
|
||||
get_processes_by_path,
|
||||
|
|
|
|||
BIN
apps/frontend/src/assets/images/default_world.png
Normal file
BIN
apps/frontend/src/assets/images/default_world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
30
apps/frontend/src/ui/components/game/WorldIcon.tsx
Normal file
30
apps/frontend/src/ui/components/game/WorldIcon.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { join } from 'pathe';
|
||||
import { type JSX, splitProps } from 'solid-js';
|
||||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
import DefaultWorld from '~assets/images/default_world.png?url';
|
||||
|
||||
export type WorldIconProps = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
world_name: string;
|
||||
cluster_name: string;
|
||||
};
|
||||
|
||||
function WorldIcon(props: WorldIconProps) {
|
||||
const [split, rest] = splitProps(props, ['cluster_name', 'world_name', 'class']);
|
||||
const { settings } = useSettingsContext();
|
||||
const path = () => convertFileSrc(join(settings().config_dir || '', 'clusters', props.cluster_name, 'saves', props.world_name, 'icon.png'));
|
||||
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
class={`w-16 h-16 rounded-md ${split.class || ''}`}
|
||||
src={path()}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = DefaultWorld;
|
||||
}}
|
||||
alt={`${props.world_name} icon`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorldIcon;
|
||||
|
|
@ -101,7 +101,6 @@ function ClusterLogs() {
|
|||
onClick={openFolder}
|
||||
children="Open Folder"
|
||||
iconLeft={<LinkExternal01Icon />}
|
||||
disabled={missingLogs()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import ClusterMods from './ClusterMods';
|
|||
import ClusterScreenshots from './ClusterScreenshots';
|
||||
import ClusterSettings from './ClusterSettings';
|
||||
import ClusterGame from './ClusterGame';
|
||||
import ClusterWorlds from './ClusterWorlds';
|
||||
import useClusterContext, { ClusterProvider } from '~ui/hooks/useCluster';
|
||||
import { supportsMods } from '~utils';
|
||||
import { tryResult } from '~ui/hooks/useCommand';
|
||||
|
|
@ -24,6 +25,7 @@ function ClusterRoutes() {
|
|||
<Route path="/logs" component={ClusterLogs} />
|
||||
<Route path="/mods" component={ClusterMods} />
|
||||
<Route path="/screenshots" component={ClusterScreenshots} />
|
||||
<Route path="/worlds" component={ClusterWorlds} />
|
||||
<Route path="/settings" component={ClusterSettings} />
|
||||
<Route path="/game" component={ClusterGame} />
|
||||
</>
|
||||
|
|
|
|||
106
apps/frontend/src/ui/pages/cluster/ClusterWorlds.tsx
Normal file
106
apps/frontend/src/ui/pages/cluster/ClusterWorlds.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { LinkExternal01Icon, Trash01Icon } from '@untitled-theme/icons-solid';
|
||||
import { join } from 'pathe';
|
||||
import { For, Show } from 'solid-js';
|
||||
import { bridge } from '~imports';
|
||||
import Button from '~ui/components/base/Button';
|
||||
import WorldIcon from '~ui/components/game/WorldIcon';
|
||||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
import useCommand from '~ui/hooks/useCommand';
|
||||
import useSettingsContext from '~ui/hooks/useSettings';
|
||||
|
||||
function ClusterWorlds() {
|
||||
const { settings } = useSettingsContext();
|
||||
const [cluster] = useClusterContext();
|
||||
const [list] = useCommand(bridge.commands.getWorlds, cluster()!.uuid!);
|
||||
|
||||
function openFolder() {
|
||||
open(join(settings().config_dir || '', 'clusters', cluster()?.path || '', 'saves'));
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Worlds</h1>
|
||||
<ScrollableContainer>
|
||||
<Show
|
||||
fallback={<div class="text-gray-400">No worlds found.</div>}
|
||||
when={list() !== undefined && list()!.length > 0}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={list()!}>
|
||||
{world_name => (
|
||||
<WorldEntry name={world_name} cluster_path={cluster()?.path || ''} />
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div class="flex flex-row justify-end items-end mt-2">
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
iconLeft={<LinkExternal01Icon />}
|
||||
children="Open Folder"
|
||||
onClick={openFolder}
|
||||
/>
|
||||
</div>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterWorlds;
|
||||
|
||||
function WorldEntry(props: { name: string; cluster_path: string }) {
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
const dir = () => join(settings().config_dir || '', 'clusters', props.cluster_path, 'saves', props.name);
|
||||
|
||||
const deleteModal = createModal(self => (
|
||||
<Modal.Delete
|
||||
{...self}
|
||||
title={`Delete '${props.name}'`}
|
||||
onDelete={() => {
|
||||
// bridge.commands.deleteWorld(props.name);
|
||||
self.hide();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
function onClick() {
|
||||
// TODO: World region viewer or whatever??
|
||||
open(dir());
|
||||
}
|
||||
|
||||
function deleteWorld(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
deleteModal.show();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed p-3 gap-3 rounded-xl flex flex-row justify-between items-center"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-x-3">
|
||||
<WorldIcon cluster_name={props.cluster_path} world_name={props.name} class="w-16 h-16 aspect-ratio-square" />
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<h3>{props.name}</h3>
|
||||
<p>Todo</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end items-center gap-x-3">
|
||||
<Button
|
||||
buttonStyle="iconDanger"
|
||||
children={<Trash01Icon />}
|
||||
onClick={deleteWorld}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -99,6 +99,12 @@ pub async fn get_logs(
|
|||
cluster_path: &ClusterPath,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Vec<LogManager>> {
|
||||
let dir = Directories::cluster_logs_dir(cluster_path).await?;
|
||||
if !dir.exists() {
|
||||
io::create_dir(dir).await?;
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut logs = Vec::new();
|
||||
get_logs_by_type(cluster_path, LogType::Info, clear, &mut logs).await?;
|
||||
get_logs_by_type(cluster_path, LogType::Crash, clear, &mut logs).await?;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod logger;
|
||||
pub mod screenshots;
|
||||
pub mod worlds;
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::utils::io;
|
||||
use crate::Result;
|
||||
|
||||
|
|
@ -23,9 +21,3 @@ pub async fn get_screenshots(cluster: &ClusterPath) -> Result<Vec<String>> {
|
|||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_screenshot_path(cluster: &ClusterPath, file_name: String) -> Result<PathBuf> {
|
||||
let dir = cluster.full_path().await?.join("screenshots");
|
||||
Ok(dir.join(file_name))
|
||||
}
|
||||
|
|
|
|||
23
packages/core/src/api/cluster/content/worlds.rs
Normal file
23
packages/core/src/api/cluster/content/worlds.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::utils::io;
|
||||
use crate::Result;
|
||||
|
||||
use crate::store::ClusterPath;
|
||||
|
||||
/// Gets a list of screenshot file names from the [`ClusterPath`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_worlds(cluster: &ClusterPath) -> Result<Vec<String>> {
|
||||
let dir = cluster.full_path().await?.join("saves");
|
||||
|
||||
if !dir.exists() {
|
||||
io::create_dir(dir).await?;
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut list = vec![];
|
||||
let mut files = io::read_dir(dir).await?;
|
||||
while let Some(file) = files.next_entry().await? {
|
||||
list.push(file.file_name().to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue