feat: Cluster editing, took too long

This commit is contained in:
LynithDev 2024-07-29 20:26:55 +02:00
parent f6b5c2f570
commit db678b9a9d
24 changed files with 324 additions and 81 deletions

View file

@ -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" ] }

View file

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

View file

@ -14,6 +14,7 @@ fn main() {
"set_default_user",
// Cluster
"create_cluster",
"edit_cluster",
"remove_cluster",
"get_cluster",
"get_clusters",

View file

@ -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"
]
}

View file

@ -12,7 +12,8 @@
},
"scripts": {
"build": "tauri build",
"dev": "tauri dev"
"dev": "tauri dev",
"tauri": "tauri"
},
"dependencies": {
"@onelauncher/core": "workspace:*",

View file

@ -11,6 +11,7 @@ permissions = [
# Cluster
"allow-create-cluster",
"allow-edit-cluster",
"allow-remove-cluster",
"allow-get-cluster",
"allow-get-clusters",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
// @ts-nocheck
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
return payload;
};

View file

@ -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) {

View file

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

View file

@ -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();

View file

@ -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;
}

View file

@ -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>
);
}

View file

@ -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);
}}
/>
);
}

View file

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

View file

@ -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>
);
}

View file

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

View file

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

View file

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

View file

@ -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
View file

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