feat: Cluster editing, took too long
This commit is contained in:
parent
f6b5c2f570
commit
db678b9a9d
24 changed files with 324 additions and 81 deletions
|
|
@ -29,7 +29,8 @@ tauri = { version = "=2.0.0-beta.23", features = [
|
|||
"rustls-tls",
|
||||
"tracing",
|
||||
"specta",
|
||||
"uuid"
|
||||
"uuid",
|
||||
"protocol-asset"
|
||||
] }
|
||||
tauri-utils = { version = "=2.0.0-beta.18" }
|
||||
tauri-build = { version = "=2.0.0-beta.18" }
|
||||
|
|
@ -37,12 +38,14 @@ tauri-codegen = { version = "=2.0.0-beta.18" }
|
|||
tauri-macros = { version = "=2.0.0-beta.18" }
|
||||
tauri-plugin = { version = "=2.0.0-beta.18" }
|
||||
tauri-runtime = { version = "=2.0.0-beta.19" }
|
||||
|
||||
tauri-plugin-authenticator = { version = "2.0.0-beta" }
|
||||
tauri-plugin-shell = { version = "2.0.0-beta" }
|
||||
tauri-plugin-window-state = { version = "2.0.0-beta" }
|
||||
tauri-plugin-single-instance = { version = "2.0.0-beta" }
|
||||
tauri-plugin-clipboard-manager = { version = "2.1.0-beta" }
|
||||
tauri-plugin-updater = { version = "2.0.0-beta", features = [ "rustls-tls" ] }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta" }
|
||||
|
||||
# rspc is updated on the v0.x branch
|
||||
rspc = { version = "=0.2.0", features = [ "specta2" ] }
|
||||
|
|
|
|||
|
|
@ -40,12 +40,14 @@ tauri-codegen = { workspace = true }
|
|||
tauri-macros = { workspace = true }
|
||||
tauri-plugin = { workspace = true }
|
||||
tauri-runtime = { workspace = true }
|
||||
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-window-state = { workspace = true }
|
||||
tauri-plugin-single-instance = { workspace = true }
|
||||
tauri-plugin-updater = { workspace = true }
|
||||
tauri-plugin-authenticator = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
|
||||
tauri-specta = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ fn main() {
|
|||
"set_default_user",
|
||||
// Cluster
|
||||
"create_cluster",
|
||||
"edit_cluster",
|
||||
"remove_cluster",
|
||||
"get_cluster",
|
||||
"get_clusters",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"window:allow-internal-toggle-maximize",
|
||||
"event:default",
|
||||
"shell:allow-open",
|
||||
"clipboard-manager:allow-write-text"
|
||||
"clipboard-manager:allow-write-text",
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev"
|
||||
"dev": "tauri dev",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@onelauncher/core": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ permissions = [
|
|||
|
||||
# Cluster
|
||||
"allow-create-cluster",
|
||||
"allow-edit-cluster",
|
||||
"allow-remove-cluster",
|
||||
"allow-get-cluster",
|
||||
"allow-get-clusters",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use interpulse::api::minecraft::Version;
|
||||
use onelauncher::data::{Loader, ManagedPackage, MinecraftCredentials, PackageData, Settings};
|
||||
use onelauncher::package::content;
|
||||
use onelauncher::store::{Cluster, ClusterPath};
|
||||
use onelauncher::{cluster, minecraft, processor, settings};
|
||||
use onelauncher::{cluster, minecraft, processor, settings, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
|
@ -24,6 +25,7 @@ macro_rules! collect_commands {
|
|||
remove_user,
|
||||
// Cluster
|
||||
create_cluster,
|
||||
edit_cluster,
|
||||
remove_cluster,
|
||||
get_cluster,
|
||||
get_clusters,
|
||||
|
|
@ -81,6 +83,33 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
|
|||
}
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn edit_cluster(
|
||||
uuid: Uuid,
|
||||
name: Option<String>,
|
||||
icon_path: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let cluster = cluster::get_by_uuid(uuid, None).await?.ok_or("cluster does not exist")?;
|
||||
|
||||
cluster::edit(&cluster.cluster_path(), move |cluster| {
|
||||
if let Some(name) = name.clone() {
|
||||
cluster.meta.name = name;
|
||||
}
|
||||
|
||||
async move {
|
||||
Ok(())
|
||||
}
|
||||
}).await?;
|
||||
|
||||
let icon_path = icon_path.and_then(|x| PathBuf::from_str(x.as_str()).ok());
|
||||
cluster::edit_icon(&cluster.cluster_path(), icon_path.as_deref()).await?;
|
||||
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn remove_cluster(uuid: Uuid) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ pub async fn run_app<F: FnOnce(&mut tauri::App) + Send + 'static>(setup: F) {
|
|||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(ext::updater::plugin())
|
||||
.manage(ext::updater::State::default())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
// .plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(api::init())
|
||||
.menu(tauri::menu::Menu::new)
|
||||
|
|
|
|||
|
|
@ -24,13 +24,11 @@
|
|||
"enable": true,
|
||||
"scope": {
|
||||
"allow": [
|
||||
"$APPDATA/caches/icons/*",
|
||||
"$APPCONFIG/caches/icons/*",
|
||||
"$CONFIG/caches/icons/*",
|
||||
"$APPDATA/db/**",
|
||||
"$DATA/OneLauncher/metadata/caches/icons/*",
|
||||
"$DATA/OneLauncher/db/**",
|
||||
"$RESOURCE/**"
|
||||
],
|
||||
"deny": ["$APPDATA/db/*.stronghold"]
|
||||
"deny": ["$DATA/OneLauncher/db/*.stronghold"]
|
||||
}
|
||||
},
|
||||
"freezePrototype": true,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"@tanstack/solid-query": "^5.51.15",
|
||||
"@tauri-apps/api": "^2.0.0-beta.13",
|
||||
"@tauri-apps/plugin-clipboard-manager": "github:tauri-apps/tauri-plugin-clipboard-manager#v2",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||
"@thisbeyond/solid-dnd": "^0.7.5",
|
||||
"@thisbeyond/solid-select": "^0.15.0",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
|
||||
return payload;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { JSX, ParentProps } from 'solid-js';
|
|||
import { For, createEffect, splitProps } from 'solid-js';
|
||||
|
||||
type SidebarProps = ParentProps & {
|
||||
state: Record<string, any>;
|
||||
base: string;
|
||||
links: Record<string, ([JSX.Element, string, string] | undefined)[]>;
|
||||
};
|
||||
|
|
@ -16,10 +15,6 @@ function Sidebar(props: SidebarProps) {
|
|||
createEffect(() => {
|
||||
if (props.base.endsWith('/'))
|
||||
throw new Error('Base should not end with a slash');
|
||||
|
||||
Object.keys(props.state).forEach((key) => {
|
||||
searchParams.set(key, props.state[key]);
|
||||
});
|
||||
});
|
||||
|
||||
function goto(href: string) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
.button {
|
||||
@apply py-1.5 px-3 rounded-lg flex flex-row justify-center items-center gap-x-2.5;
|
||||
|
||||
&:disabled {
|
||||
@apply 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;
|
||||
|
|
@ -17,7 +25,7 @@
|
|||
}
|
||||
|
||||
&__iconDanger {
|
||||
@apply bg-op-10;
|
||||
@apply bg-op-50;
|
||||
}
|
||||
|
||||
&__ghost {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { JSX, ParentProps } from 'solid-js';
|
||||
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
||||
import type { Accessor, JSX, ParentProps } from 'solid-js';
|
||||
import { createEffect, createSignal, mergeProps, on, splitProps, untrack } from 'solid-js';
|
||||
import styles from './Button.module.scss';
|
||||
|
||||
type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & ParentProps & {
|
||||
|
|
@ -23,13 +23,15 @@ function Button(props: ButtonProps) {
|
|||
}
|
||||
|
||||
type ButtonToggleProps = ButtonProps & {
|
||||
defaultChecked?: boolean;
|
||||
checked?: Accessor<boolean>;
|
||||
onChecked?: (checked: boolean) => any;
|
||||
};
|
||||
|
||||
Button.Toggle = (props: ButtonToggleProps) => {
|
||||
const [checked, setChecked] = createSignal(props.defaultChecked || false);
|
||||
const [split, rest] = splitProps(props, ['defaultChecked', 'onChecked', 'onClick', 'class']);
|
||||
const [split, rest] = splitProps(props, ['checked', 'onChecked', 'onClick', 'class']);
|
||||
const [checked, setChecked] = createSignal(untrack(() => split.checked?.()) || false);
|
||||
|
||||
createEffect(on(() => split.checked?.(), newValue => setChecked(newValue || false)));
|
||||
|
||||
function toggle() {
|
||||
const newValue = !checked();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
.textfield {
|
||||
@apply bg-component-bg [&:not(:focus-within)]:hover:bg-component-bg-hover focus-within:bg-component-bg-pressed disabled:bg-component-bg-disabled;
|
||||
@apply border border-gray-05 text-fg-primary fill-fg-primary;
|
||||
@apply rounded-lg px-2.5 py-1.5 max-h-8;
|
||||
@apply rounded-lg px-2.5 py-1.5 min-h-8;
|
||||
@apply flex flex-row items-center justify-start gap-x-1.5;
|
||||
@apply text-sm;
|
||||
|
||||
&.invalid {
|
||||
@apply border-danger;
|
||||
}
|
||||
|
||||
|
||||
& input {
|
||||
&::selection {
|
||||
@apply bg-brand/30;
|
||||
}
|
||||
|
||||
|
||||
@apply bg-transparent outline-none appearance-none p-0 m-0 box-border w-full;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,33 +8,34 @@ type TextFieldProps = {
|
|||
inputFilter?: (value: string) => boolean;
|
||||
onValidInput?: (value: string) => any;
|
||||
onValidSubmit?: (value: string) => any;
|
||||
labelClass?: string;
|
||||
} & JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
function TextField(props: TextFieldProps) {
|
||||
const [fieldProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'inputFilter', 'onValidInput', 'onValidSubmit']);
|
||||
const [split, rest] = splitProps(props, ['iconLeft', 'iconRight', 'inputFilter', 'onValidInput', 'onValidSubmit', 'labelClass']);
|
||||
const [isValid, setIsValid] = createSignal(true);
|
||||
const id = createUniqueId();
|
||||
|
||||
function validate(e: Event & { currentTarget: HTMLInputElement }) {
|
||||
if (!fieldProps.inputFilter)
|
||||
if (!split.inputFilter)
|
||||
return;
|
||||
|
||||
const value = e.currentTarget.value;
|
||||
const valid = fieldProps.inputFilter(value);
|
||||
const valid = split.inputFilter(value);
|
||||
setIsValid(valid);
|
||||
|
||||
if (valid && fieldProps.onValidInput)
|
||||
fieldProps.onValidInput(value);
|
||||
if (valid && split.onValidInput)
|
||||
split.onValidInput(value);
|
||||
}
|
||||
|
||||
function onSubmit(e: Event & { currentTarget: HTMLInputElement }) {
|
||||
if (isValid() && fieldProps.onValidSubmit)
|
||||
fieldProps.onValidSubmit(e.currentTarget.value);
|
||||
if (isValid() && split.onValidSubmit)
|
||||
split.onValidSubmit(e.currentTarget.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<label for={id} class={`${styles.textfield} ${isValid() ? '' : styles.invalid}`}>
|
||||
{fieldProps.iconLeft && <span class={styles.icon}>{fieldProps.iconLeft}</span>}
|
||||
<label for={id} class={`${styles.textfield} ${isValid() ? '' : styles.invalid} ${split.labelClass || ''}`}>
|
||||
{split.iconLeft && <span class={styles.icon}>{split.iconLeft}</span>}
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
|
|
@ -42,7 +43,7 @@ function TextField(props: TextFieldProps) {
|
|||
onChange={onSubmit}
|
||||
{...rest}
|
||||
/>
|
||||
{fieldProps.iconRight && <span class={styles.icon}>{fieldProps.iconRight}</span>}
|
||||
{split.iconRight && <span class={styles.icon}>{split.iconRight}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,39 @@
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { type JSX, splitProps } from 'solid-js';
|
||||
import defaultCover from '~assets/images/default_instance_cover.jpg';
|
||||
import type { Cluster } from '~bindings';
|
||||
|
||||
type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
cluster: Cluster;
|
||||
cluster: Cluster | undefined;
|
||||
override?: string;
|
||||
};
|
||||
|
||||
function ClusterCover(props: ClusterCoverProps) {
|
||||
const [split, rest] = splitProps(props, ['cluster']);
|
||||
const [split, rest] = splitProps(props, ['cluster', 'override', 'onError']);
|
||||
|
||||
const image = () => {
|
||||
const url = split.override || split.cluster?.meta.icon_url || split.cluster?.meta.icon;
|
||||
|
||||
if (url === undefined || url === null)
|
||||
return defaultCover;
|
||||
|
||||
if (url.startsWith('/'))
|
||||
return convertFileSrc(url);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
return (
|
||||
<img {...rest} src={split.cluster.meta.icon_url || defaultCover} />
|
||||
<img
|
||||
{...rest}
|
||||
src={image()}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = split.override ? convertFileSrc(split.override) : defaultCover;
|
||||
if (split.onError)
|
||||
// @ts-expect-error -- jsx stuff
|
||||
split.onError!(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { type Context, type ParentProps, type Resource, Show, createContext, useContext } from 'solid-js';
|
||||
import { type Context, type ParentProps, type ResourceReturn, Show, createContext, useContext } from 'solid-js';
|
||||
import useCommand from './useCommand';
|
||||
import type { Cluster } from '~bindings';
|
||||
import { bridge } from '~imports';
|
||||
|
||||
const ClusterContext = createContext<Cluster>() as Context<Cluster>;
|
||||
const ClusterContext = createContext() as Context<ResourceReturn<Cluster>>;
|
||||
|
||||
export function getCluster(uuid: string | undefined | null): Resource<Cluster> | undefined {
|
||||
export function getCluster(uuid: string | undefined | null): ResourceReturn<Cluster> | undefined {
|
||||
if (typeof uuid !== 'string' || uuid.length === 0)
|
||||
return undefined;
|
||||
|
||||
const [resource] = useCommand(bridge.commands.getCluster, uuid);
|
||||
const resource = useCommand(bridge.commands.getCluster, uuid);
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
|
@ -18,8 +18,8 @@ export function ClusterProvider(props: ParentProps & { uuid: string | undefined
|
|||
const cluster = getCluster(props.uuid);
|
||||
|
||||
return (
|
||||
<Show when={cluster !== undefined && cluster() !== undefined}>
|
||||
<ClusterContext.Provider value={cluster!()!}>
|
||||
<Show when={cluster !== undefined && cluster![0]() !== undefined}>
|
||||
<ClusterContext.Provider value={cluster!}>
|
||||
{props.children}
|
||||
</ClusterContext.Provider>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { PlayIcon, Share07Icon, Trash01Icon } from '@untitled-theme/icons-solid';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import { Edit02Icon, ImagePlusIcon, PlayIcon, Save01Icon, Share07Icon, Trash01Icon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { Show, createSignal, untrack } from 'solid-js';
|
||||
import type { BeforeLeaveEventArgs } from '@solidjs/router';
|
||||
import { useBeforeLeave, useNavigate } from '@solidjs/router';
|
||||
import * as dialog from '@tauri-apps/plugin-dialog';
|
||||
import ClusterCover from '../../components/game/ClusterCover';
|
||||
import LoaderIcon from '../../components/game/LoaderIcon';
|
||||
import Button from '../../components/base/Button';
|
||||
|
|
@ -12,14 +14,15 @@ 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';
|
||||
|
||||
function ClusterOverview() {
|
||||
const cluster = useClusterContext();
|
||||
const [cluster, { refetch }] = useClusterContext();
|
||||
const [deleteVisible, setDeleteVisible] = createSignal(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function deleteCluster() {
|
||||
await bridge.commands.removeCluster(cluster.uuid);
|
||||
await bridge.commands.removeCluster(cluster()!.uuid);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +30,7 @@ function ClusterOverview() {
|
|||
<Sidebar.Page>
|
||||
<h1>Overview</h1>
|
||||
<ScrollableContainer>
|
||||
<Banner {...cluster} showDeletePrompt={() => setDeleteVisible(true)} />
|
||||
<Banner cluster={cluster()!} refetch={refetch} showDeletePrompt={() => setDeleteVisible(true)} />
|
||||
</ScrollableContainer>
|
||||
|
||||
<Modal.Delete
|
||||
|
|
@ -39,49 +42,159 @@ function ClusterOverview() {
|
|||
);
|
||||
}
|
||||
|
||||
function Banner(props: Cluster & { showDeletePrompt: () => void }) {
|
||||
async function launch() {
|
||||
const [_uuid, _pid] = await tryResult(bridge.commands.runCluster, props.uuid);
|
||||
interface BannerProps {
|
||||
cluster: Cluster;
|
||||
refetch: () => void;
|
||||
showDeletePrompt: () => void;
|
||||
}
|
||||
|
||||
// console.log(uuid, pid);
|
||||
function Banner(props: BannerProps) {
|
||||
const [editMode, setEditMode] = createSignal(false);
|
||||
const [newName, setNewName] = createSignal('');
|
||||
const [newCover, setNewCover] = createSignal('');
|
||||
|
||||
const [modalVisible, setModalVisible] = createSignal(false);
|
||||
|
||||
async function launch() {
|
||||
const [_uuid, _pid] = await tryResult(bridge.commands.runCluster, props.cluster.uuid);
|
||||
}
|
||||
|
||||
useBeforeLeave((e) => {
|
||||
if (editMode() && madeChanges()) {
|
||||
e.preventDefault();
|
||||
setModalVisible(true);
|
||||
}
|
||||
});
|
||||
|
||||
async function launchFilePicker() {
|
||||
const selected = await dialog.open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
}],
|
||||
});
|
||||
|
||||
if (selected === null)
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5">
|
||||
<div class="w-48 rounded-lg overflow-hidden border border-gray-10">
|
||||
<ClusterCover cluster={props} />
|
||||
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5 h-37">
|
||||
<div class="rounded-lg overflow-hidden border border-gray-10 relative h-full w-auto aspect-ratio-video">
|
||||
<Show when={editMode()}>
|
||||
<div
|
||||
onClick={launchFilePicker}
|
||||
class="bg-black/50 opacity-50 hover:opacity-100 w-full h-full absolute flex justify-center items-center"
|
||||
>
|
||||
<ImagePlusIcon class="w-12 h-12" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<ClusterCover override={newCover()} cluster={props.cluster} class="h-full w-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 text-fg-primary">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl">{props.meta.name}</h2>
|
||||
<div class="flex flex-col flex-1 gap-y-.5 justify-between text-fg-primary">
|
||||
<div>
|
||||
<Show
|
||||
when={editMode()}
|
||||
fallback={
|
||||
<h2 class="text-2xl">{props.cluster.meta.name}</h2>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
placeholder={props.cluster.meta.name}
|
||||
labelClass="h-10"
|
||||
class="text-xl font-bold"
|
||||
onChange={e => updateName(e.target.value)}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
|
||||
<div class={`flex flex-1 flex-col justify-between items-start ${editMode() ? 'opacity-20' : ''}`}>
|
||||
<span class="flex flex-row items-center gap-x-1">
|
||||
<LoaderIcon loader={props.meta.loader} class="w-5" />
|
||||
<span>{upperFirst(props.meta.loader || 'unknown')}</span>
|
||||
{props.meta.loader_version && <span>{props.meta.loader_version.id}</span>}
|
||||
<span>{props.meta.mc_version}</span>
|
||||
<LoaderIcon loader={props.cluster.meta.loader} class="w-5" />
|
||||
<span>{upperFirst(props.cluster.meta.loader || 'unknown')}</span>
|
||||
{props.cluster.meta.loader_version && <span>{props.cluster.meta.loader_version.id}</span>}
|
||||
<span>{props.cluster.meta.mc_version}</span>
|
||||
</span>
|
||||
<span class="text-xs text-fg-secondary">
|
||||
Played for
|
||||
{' '}
|
||||
<b>{((props.cluster.meta.overall_played || 0 as unknown as bigint)).toString()}</b>
|
||||
{' '}
|
||||
hours
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-fg-secondary">
|
||||
Played for
|
||||
{' '}
|
||||
<b>{((props.meta.overall_played || 0 as unknown as bigint)).toString()}</b>
|
||||
{' '}
|
||||
hours
|
||||
{/* TODO: Ask Pauline if this is measured in seconds or milliseconds */}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-end gap-x-2.5 *:h-8">
|
||||
<Button
|
||||
buttonStyle="iconDanger"
|
||||
children={<Trash01Icon />}
|
||||
onClick={props.showDeletePrompt}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonStyle="iconSecondary"
|
||||
children={<Share07Icon />}
|
||||
disabled={editMode()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonStyle="iconDanger"
|
||||
children={<Trash01Icon />}
|
||||
onClick={props.showDeletePrompt}
|
||||
disabled={editMode()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
|
@ -90,8 +203,46 @@ function Banner(props: Cluster & { showDeletePrompt: () => void }) {
|
|||
children="Launch"
|
||||
class="!w-auto"
|
||||
onClick={launch}
|
||||
disabled={editMode()}
|
||||
/>
|
||||
|
||||
<Button.Toggle
|
||||
buttonStyle="iconSecondary"
|
||||
children={(
|
||||
<Show
|
||||
when={editMode() === false}
|
||||
fallback={<Save01Icon />}
|
||||
children={<Edit02Icon />}
|
||||
/>
|
||||
)}
|
||||
checked={editMode}
|
||||
onChecked={toggleEditMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal.Simple
|
||||
title="Save Changes?"
|
||||
visible={modalVisible}
|
||||
setVisible={setModalVisible}
|
||||
children="Do you want to save your changes?"
|
||||
buttons={[
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
children="Cancel"
|
||||
onClick={() => setModalVisible(false)}
|
||||
/>,
|
||||
<Button
|
||||
buttonStyle="danger"
|
||||
children="No"
|
||||
onClick={dontSave}
|
||||
/>,
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
children="Yes"
|
||||
onClick={save}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,17 +46,16 @@ function ClusterRoot(props: ParentProps) {
|
|||
}
|
||||
|
||||
function ClusterSidebar() {
|
||||
const cluster = useClusterContext();
|
||||
const [cluster] = useClusterContext();
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
base="/clusters"
|
||||
state={{ id: cluster.uuid }}
|
||||
links={{
|
||||
Cluster: [
|
||||
[<EyeIcon />, 'Overview', '/'],
|
||||
// TODO: Better way of checking mods
|
||||
(cluster!.meta.loader !== 'vanilla' ? [<PackagePlusIcon />, 'Mods', '/mods'] : undefined),
|
||||
(cluster()?.meta.loader !== 'vanilla' ? [<PackagePlusIcon />, 'Mods', '/mods'] : undefined),
|
||||
[<Image03Icon />, 'Screenshots', '/screenshots'],
|
||||
[<Globe04Icon />, 'Worlds', '/worlds'],
|
||||
[<File06Icon />, 'Logs', '/logs'],
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub async fn create_cluster(
|
|||
if let Some(ref icon) = icon {
|
||||
let bytes = io::read(state.directories.caches_dir().await.join(icon)).await?;
|
||||
cluster
|
||||
.set_icon(
|
||||
.set_icon_bytes(
|
||||
&state.directories.caches_dir().await,
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ pub async fn edit_icon(path: &ClusterPath, icon_path: Option<&Path>) -> crate::R
|
|||
match clusters.0.get_mut(path) {
|
||||
Some(ref mut cluster) => {
|
||||
cluster
|
||||
.set_icon(
|
||||
.set_icon_bytes(
|
||||
&state.directories.caches_dir().await,
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
|
|
|
|||
|
|
@ -386,7 +386,21 @@ impl Cluster {
|
|||
|
||||
/// Set the icon [`bytes::Bytes`] for this cluster.
|
||||
#[tracing::instrument(skip(self, io_semaphore, icon))]
|
||||
pub async fn set_icon<'a>(
|
||||
pub async fn set_icon_path<'a>(
|
||||
&'a mut self,
|
||||
cache_path: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
icon: &Path,
|
||||
file_name: &str,
|
||||
) -> crate::Result<()> {
|
||||
let icon = io::read(icon).await?;
|
||||
self.set_icon_bytes(cache_path, io_semaphore, icon.into(), file_name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the icon [`bytes::Bytes`] for this cluster.
|
||||
#[tracing::instrument(skip(self, io_semaphore, icon))]
|
||||
pub async fn set_icon_bytes<'a>(
|
||||
&'a mut self,
|
||||
cache_path: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -96,6 +96,9 @@ importers:
|
|||
'@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
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: 2.0.0-beta.7
|
||||
version: 2.0.0-beta.7
|
||||
'@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
|
||||
|
|
@ -1105,6 +1108,9 @@ packages:
|
|||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-clipboard-manager/tar.gz/d503abbce40595d0af6b36c10439b85c640b4c53}
|
||||
version: 2.1.0-beta.5
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.0.0-beta.7':
|
||||
resolution: {integrity: sha512-myywwpsKbquDDzl5zaOmmLLv5O8EJ/GgHDAoVSPwO97R4iWzkDvj3HFF91tNh7i25Tu/bP6jYPAdZA1NCRxxtg==}
|
||||
|
||||
'@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
|
||||
|
|
@ -4239,6 +4245,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-beta.15
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.0.0-beta.7':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-beta.15
|
||||
|
||||
'@tauri-apps/plugin-shell@https://codeload.github.com/tauri-apps/tauri-plugin-shell/tar.gz/adb938ddf434547ae35f74dc1a080908da03ea96':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-beta.15
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue