[ENG-1365] Move to Trash (#2318)

* Working System Trash Connection

* small changes and prettier

---------

Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
Arnab Chakraborty 2024-04-12 01:01:37 -04:00 committed by GitHub
parent 9f5396133b
commit 66063d22c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 233 additions and 52 deletions

View file

@ -172,6 +172,7 @@ Also ensure that Rosetta is installed, as a few of our dependencies require it.
#### `ModuleNotFoundError: No module named 'distutils'` #### `ModuleNotFoundError: No module named 'distutils'`
If you run into this issue, or some other error involving `node-gyp`: If you run into this issue, or some other error involving `node-gyp`:
``` ```
File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module> File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module>
import gyp # noqa: E402 import gyp # noqa: E402

26
Cargo.lock generated
View file

@ -8276,6 +8276,7 @@ dependencies = [
"tracing-appender", "tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"tracing-test", "tracing-test",
"trash",
"uuid", "uuid",
"webp", "webp",
] ]
@ -10531,6 +10532,22 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "trash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1a7a9a17d3b004898be42be29a4c18d5a4cf008b5cdf72d69b1945dfcb158a"
dependencies = [
"chrono",
"libc",
"log",
"objc",
"once_cell",
"scopeguard",
"url",
"windows 0.44.0",
]
[[package]] [[package]]
name = "treediff" name = "treediff"
version = "4.0.2" version = "4.0.2"
@ -11267,6 +11284,15 @@ dependencies = [
"windows_x86_64_msvc 0.39.0", "windows_x86_64_msvc 0.39.0",
] ]
[[package]]
name = "windows"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.48.0" version = "0.48.0"

View file

@ -132,6 +132,7 @@ static_assertions = "1.1.0"
sysinfo = "0.29.10" sysinfo = "0.29.10"
tar = "0.4.40" tar = "0.4.40"
tower-service = "0.3.2" tower-service = "0.3.2"
trash = "4.1.0"
# Override features of transitive dependencies # Override features of transitive dependencies
[dependencies.openssl] [dependencies.openssl]

View file

@ -27,6 +27,7 @@ use specta::Type;
use tokio::{fs, io}; use tokio::{fs, io};
use tokio_stream::{wrappers::ReadDirStream, StreamExt}; use tokio_stream::{wrappers::ReadDirStream, StreamExt};
use tracing::{error, warn}; use tracing::{error, warn};
use trash;
use super::{ use super::{
files::{create_directory, FromPattern}, files::{create_directory, FromPattern},
@ -147,6 +148,35 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
Ok(()) Ok(())
}) })
}) })
.procedure("moveToTrash", {
R.with2(library())
.mutation(|(_, library), paths: Vec<PathBuf>| async move {
paths
.into_iter()
.map(|path| async move {
match fs::metadata(&path).await {
Ok(_) => {
trash::delete(&path).unwrap();
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(FileIOError::from((
path,
e,
"Failed to get file metadata for deletion",
))),
}
})
.collect::<Vec<_>>()
.try_join()
.await?;
invalidate_query!(library, "search.ephemeralPaths");
Ok(())
})
})
.procedure("copyFiles", { .procedure("copyFiles", {
R.with2(library()) R.with2(library())
.mutation(|(_, library), args: EphemeralFileSystemOps| async move { .mutation(|(_, library), args: EphemeralFileSystemOps| async move {

View file

@ -44,6 +44,7 @@ use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use tokio::{fs, io, task::spawn_blocking}; use tokio::{fs, io, task::spawn_blocking};
use tracing::{error, warn}; use tracing::{error, warn};
use trash;
use super::{Ctx, R}; use super::{Ctx, R};
@ -515,6 +516,53 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
} }
}) })
}) })
.procedure("moveToTrash", {
R.with2(library())
.mutation(|(node, library), args: OldFileDeleterJobInit| async move {
match args.file_path_ids.len() {
0 => Ok(()),
1 => {
let (maybe_location, maybe_file_path) = library
.db
._batch((
library
.db
.location()
.find_unique(location::id::equals(args.location_id))
.select(location::select!({ path })),
library
.db
.file_path()
.find_unique(file_path::id::equals(args.file_path_ids[0]))
.select(file_path_to_isolate::select()),
))
.await?;
let location_path = maybe_location
.ok_or(LocationError::IdNotFound(args.location_id))?
.path
.ok_or(LocationError::MissingPath(args.location_id))?;
let file_path = maybe_file_path.ok_or(LocationError::FilePath(
FilePathError::IdNotFound(args.file_path_ids[0]),
))?;
let full_path = Path::new(&location_path).join(
IsolatedFilePathData::try_from(&file_path)
.map_err(LocationError::MissingField)?,
);
trash::delete(&full_path).unwrap();
Ok(())
}
_ => Job::new(args)
.spawn(&node, &library)
.await
.map_err(Into::into),
}
})
})
.procedure("convertImage", { .procedure("convertImage", {
#[derive(Type, Deserialize)] #[derive(Type, Deserialize)]
struct ConvertImageArgs { struct ConvertImageArgs {

View file

@ -1,4 +1,8 @@
import { AppWindow, ArrowSquareOut, CaretRight, ClipboardText } from '@phosphor-icons/react'; import { AppWindow, ArrowSquareOut, CaretRight, ClipboardText } from '@phosphor-icons/react';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { import {
getExplorerItemData, getExplorerItemData,
getIndexedItemFilePath, getIndexedItemFilePath,
@ -6,13 +10,9 @@ import {
useLibraryQuery useLibraryQuery
} from '@sd/client'; } from '@sd/client';
import { ContextMenu } from '@sd/ui'; import { ContextMenu } from '@sd/ui';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { useTabsContext } from '~/TabsContext';
import { Icon } from '~/components'; import { Icon } from '~/components';
import { useIsDark, useLocale, useOperatingSystem } from '~/hooks'; import { useIsDark, useLocale, useOperatingSystem } from '~/hooks';
import { useTabsContext } from '~/TabsContext';
import { usePlatform } from '~/util/Platform'; import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from './Context'; import { useExplorerContext } from './Context';
@ -240,7 +240,7 @@ const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
<ContextMenu.Item <ContextMenu.Item
onClick={() => navigator.clipboard.writeText(osPath)} onClick={() => navigator.clipboard.writeText(osPath)}
icon={ClipboardText} icon={ClipboardText}
label={t("copy_as_path")} label={t('copy_as_path')}
/> />
</ContextMenu.Root> </ContextMenu.Root>
); );

View file

@ -47,6 +47,8 @@ export default (props: Props) => {
const { t } = useLocale(); const { t } = useLocale();
const deleteFile = useLibraryMutation('files.deleteFiles'); const deleteFile = useLibraryMutation('files.deleteFiles');
const deleteEphemeralFile = useLibraryMutation('ephemeralFiles.deleteFiles'); const deleteEphemeralFile = useLibraryMutation('ephemeralFiles.deleteFiles');
const moveToTrashFile = useLibraryMutation('files.moveToTrash');
const moveToTrashEphemeralFile = useLibraryMutation('ephemeralFiles.moveToTrash');
const form = useZodForm(); const form = useZodForm();
const { dirCount = 0, fileCount = 0, indexedArgs, ephemeralArgs } = props; const { dirCount = 0, fileCount = 0, indexedArgs, ephemeralArgs } = props;
@ -76,23 +78,46 @@ export default (props: Props) => {
await deleteEphemeralFile.mutateAsync(paths); await deleteEphemeralFile.mutateAsync(paths);
} }
})} })}
onSubmitSecond={form.handleSubmit(async () => {
if (indexedArgs != undefined) {
console.log(
'DEBUG: DeleteDialog.tsx: onSubmitSecond (Move to Trash) -> Indexed Files'
);
const { locationId, rescan, pathIds } = indexedArgs;
await moveToTrashFile.mutateAsync({
location_id: locationId,
file_path_ids: pathIds
});
rescan?.();
}
if (ephemeralArgs != undefined) {
console.log(
'DEBUG: DeleteDialog.tsx: onSubmitSecond (Move to Trash) -> Ephemeral Files'
);
const { paths } = ephemeralArgs;
await moveToTrashEphemeralFile.mutateAsync(paths);
}
})}
icon={<Icon theme="light" name={icon} size={28} />} icon={<Icon theme="light" name={icon} size={28} />}
dialog={useDialog(props)} dialog={useDialog(props)}
title={t('delete_dialog_title', { prefix, type })} title={t('delete_dialog_title', { prefix, type })}
description={description} description={description}
loading={deleteFile.isLoading} loading={deleteFile.isLoading}
ctaLabel={t('delete')} ctaLabel={t('delete_forever')}
ctaSecondLabel={t('move_to_trash')}
ctaDanger ctaDanger
className="w-[200px]" className="w-[200px]"
> >
<Tooltip label={t('coming_soon')}> {/* <Tooltip label={t('coming_soon')}>
<div className="flex items-center pt-2 opacity-50"> <div className="flex items-center pt-2 opacity-50">
<CheckBox disabled className="!mt-0" /> <CheckBox disabled className="!mt-0" />
<p className="text-sm text-ink-dull"> <p className="text-sm text-ink-dull">
Delete all matching {type.endsWith('s') ? type : type + 's'} Delete all matching {type.endsWith('s') ? type : type + 's'}
</p> </p>
</div> </div>
</Tooltip> </Tooltip> */}
</Dialog> </Dialog>
); );
}; };

View file

@ -104,7 +104,7 @@
"delete_rule": "Выдаліць правіла", "delete_rule": "Выдаліць правіла",
"delete_tag": "Выдаліць тэг", "delete_tag": "Выдаліць тэг",
"delete_tag_description": "Вы ўпэўнены, што хочаце выдаліць гэты тэг? Гэта дзеянне няможна скасаваць, і тэгнутые файлы будуць адлучаны.", "delete_tag_description": "Вы ўпэўнены, што хочаце выдаліць гэты тэг? Гэта дзеянне няможна скасаваць, і тэгнутые файлы будуць адлучаны.",
"delete_warning": "Папярэджанне: гэта выдаліць ваш {{type}} назаўжды, у нас пакуль няма сметніцы.", "delete_warning": "гэта выдаліць ваш {{type}} назаўжды, у нас пакуль няма сметніцы.",
"description": "Апісанне", "description": "Апісанне",
"deselect": "Скасаваць выбар", "deselect": "Скасаваць выбар",
"details": "Падрабязней", "details": "Падрабязней",

View file

@ -104,7 +104,7 @@
"delete_rule": "Regel löschen", "delete_rule": "Regel löschen",
"delete_tag": "Tag löschen", "delete_tag": "Tag löschen",
"delete_tag_description": "Sind Sie sicher, dass Sie diesen Tag löschen möchten? Dies kann nicht rückgängig gemacht werden, und getaggte Dateien werden nicht mehr verlinkt.", "delete_tag_description": "Sind Sie sicher, dass Sie diesen Tag löschen möchten? Dies kann nicht rückgängig gemacht werden, und getaggte Dateien werden nicht mehr verlinkt.",
"delete_warning": "Warnung: Dies wird Ihre {{type}} für immer löschen, wir haben noch keinen Papierkorb...", "delete_warning": "Dies wird Ihre {{type}} für immer löschen, wir haben noch keinen Papierkorb...",
"description": "Beschreibung", "description": "Beschreibung",
"deselect": "Abwählen", "deselect": "Abwählen",
"details": "Details", "details": "Details",

View file

@ -64,6 +64,10 @@
"copy_path_to_clipboard": "Copy path to clipboard", "copy_path_to_clipboard": "Copy path to clipboard",
"copy_success": "Items copied", "copy_success": "Items copied",
"create": "Create", "create": "Create",
"create_file_error": "Error creating file",
"create_file_success": "Created new file: {{name}}",
"create_folder_error": "Error creating folder",
"create_folder_success": "Created new folder: {{name}}",
"create_library": "Create a Library", "create_library": "Create a Library",
"create_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.", "create_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
"create_new_library": "Create new library", "create_new_library": "Create new library",
@ -82,11 +86,16 @@
"cut_object": "Cut object", "cut_object": "Cut object",
"cut_success": "Items cut", "cut_success": "Items cut",
"data_folder": "Data Folder", "data_folder": "Data Folder",
"date_accessed": "Date Accessed",
"date_created": "Date Created",
"date_indexed": "Date Indexed",
"date_modified": "Date Modified",
"debug_mode": "Debug mode", "debug_mode": "Debug mode",
"debug_mode_description": "Enable extra debugging features within the app.", "debug_mode_description": "Enable extra debugging features within the app.",
"default": "Default", "default": "Default",
"delete": "Delete", "delete": "Delete",
"delete_dialog_title": "Delete {{prefix}} {{type}}", "delete_dialog_title": "Delete {{prefix}} {{type}}",
"delete_forever": "Delete Forever",
"delete_info": "This will not delete the actual folder on disk. Preview media will be deleted.", "delete_info": "This will not delete the actual folder on disk. Preview media will be deleted.",
"delete_library": "Delete Library", "delete_library": "Delete Library",
"delete_library_description": "This is permanent, your files will not be deleted, only the Spacedrive library.", "delete_library_description": "This is permanent, your files will not be deleted, only the Spacedrive library.",
@ -96,7 +105,7 @@
"delete_rule": "Delete rule", "delete_rule": "Delete rule",
"delete_tag": "Delete Tag", "delete_tag": "Delete Tag",
"delete_tag_description": "Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked.", "delete_tag_description": "Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked.",
"delete_warning": "Warning: This will delete your {{type}} forever, we don't have a trash can yet...", "delete_warning": "This will delete your {{type}}. This action cannot be undone as of right now. If you Move to Trash, you can restore it later. If you Delete Forever, it will be gone forever.",
"description": "Description", "description": "Description",
"deselect": "Deselect", "deselect": "Deselect",
"details": "Details", "details": "Details",
@ -121,7 +130,10 @@
"edit": "Edit", "edit": "Edit",
"edit_library": "Edit Library", "edit_library": "Edit Library",
"edit_location": "Edit Location", "edit_location": "Edit Location",
"empty_file": "Empty file",
"enable_networking": "Enable Networking", "enable_networking": "Enable Networking",
"enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.",
"enable_networking_description_required": "Required for library sync or Spacedrop!",
"encrypt": "Encrypt", "encrypt": "Encrypt",
"encrypt_library": "Encrypt Library", "encrypt_library": "Encrypt Library",
"encrypt_library_coming_soon": "Library encryption coming soon", "encrypt_library_coming_soon": "Library encryption coming soon",
@ -200,6 +212,7 @@
"hide_in_sidebar_description": "Prevent this tag from showing in the sidebar of the app.", "hide_in_sidebar_description": "Prevent this tag from showing in the sidebar of the app.",
"hide_location_from_view": "Hide location and contents from view", "hide_location_from_view": "Hide location and contents from view",
"home": "Home", "home": "Home",
"icon_size": "Icon size",
"image_labeler_ai_model": "Image label recognition AI model", "image_labeler_ai_model": "Image label recognition AI model",
"image_labeler_ai_model_description": "The model used to recognize objects in images. Larger models are more accurate but slower.", "image_labeler_ai_model_description": "The model used to recognize objects in images. Larger models are more accurate but slower.",
"import": "Import", "import": "Import",
@ -211,8 +224,6 @@
"install_update": "Install Update", "install_update": "Install Update",
"installed": "Installed", "installed": "Installed",
"item_size": "Item size", "item_size": "Item size",
"icon_size": "Icon size",
"text_size": "Text size",
"item_with_count_one": "{{count}} item", "item_with_count_one": "{{count}} item",
"item_with_count_other": "{{count}} items", "item_with_count_other": "{{count}} items",
"job_has_been_canceled": "Job has been canceled.", "job_has_been_canceled": "Job has been canceled.",
@ -250,6 +261,7 @@
"location_connected_tooltip": "Location is being watched for changes", "location_connected_tooltip": "Location is being watched for changes",
"location_disconnected_tooltip": "Location is not being watched for changes", "location_disconnected_tooltip": "Location is not being watched for changes",
"location_display_name_info": "The name of this Location, this is what will be displayed in the sidebar. Will not rename the actual folder on disk.", "location_display_name_info": "The name of this Location, this is what will be displayed in the sidebar. Will not rename the actual folder on disk.",
"location_empty_notice_message": "No files found here",
"location_is_already_linked": "Location is already linked", "location_is_already_linked": "Location is already linked",
"location_path_info": "The path to this Location, this is where the files will be stored on disk.", "location_path_info": "The path to this Location, this is where the files will be stored on disk.",
"location_type": "Location Type", "location_type": "Location Type",
@ -259,9 +271,11 @@
"locations": "Locations", "locations": "Locations",
"locations_description": "Manage your storage locations.", "locations_description": "Manage your storage locations.",
"lock": "Lock", "lock": "Lock",
"log_in": "Log in",
"log_in_with_browser": "Log in with browser", "log_in_with_browser": "Log in with browser",
"log_out": "Log out", "log_out": "Log out",
"logged_in_as": "Logged in as {{email}}", "logged_in_as": "Logged in as {{email}}",
"logging_in": "Logging in...",
"logout": "Logout", "logout": "Logout",
"manage_library": "Manage Library", "manage_library": "Manage Library",
"managed": "Managed", "managed": "Managed",
@ -279,6 +293,7 @@
"move_back_within_quick_preview": "Move back within quick preview", "move_back_within_quick_preview": "Move back within quick preview",
"move_files": "Move Files", "move_files": "Move Files",
"move_forward_within_quick_preview": "Move forward within quick preview", "move_forward_within_quick_preview": "Move forward within quick preview",
"move_to_trash": "Move to Trash",
"name": "Name", "name": "Name",
"navigate_back": "Navigate back", "navigate_back": "Navigate back",
"navigate_backwards": "Navigate backwards", "navigate_backwards": "Navigate backwards",
@ -296,19 +311,14 @@
"networking_port_description": "The port for Spacedrive's Peer-to-peer networking to communicate on. You should leave this disabled unless you have a restrictive firewall. Do not expose to the internet!", "networking_port_description": "The port for Spacedrive's Peer-to-peer networking to communicate on. You should leave this disabled unless you have a restrictive firewall. Do not expose to the internet!",
"new": "New", "new": "New",
"new_folder": "Folder", "new_folder": "Folder",
"text_file": "Text File",
"empty_file": "Empty file",
"create_folder_error": "Error creating folder",
"create_file_error": "Error creating file",
"create_folder_success": "Created new folder: {{name}}",
"create_file_success": "Created new file: {{name}}",
"new_library": "New library", "new_library": "New library",
"new_location": "New location", "new_location": "New location",
"new_location_web_description": "As you are using the browser version of Spacedrive you will (for now) need to specify an absolute URL of a directory local to the remote node.", "new_location_web_description": "As you are using the browser version of Spacedrive you will (for now) need to specify an absolute URL of a directory local to the remote node.",
"new_tab": "New Tab", "new_tab": "New Tab",
"new_tag": "New tag", "new_tag": "New tag",
"new_update_available": "New Update Available!", "new_update_available": "New Update Available!",
"location_empty_notice_message": "No files found here", "no_favorite_items": "No favorite items",
"no_items_found": "No items found",
"no_jobs": "No jobs.", "no_jobs": "No jobs.",
"no_labels": "No labels", "no_labels": "No labels",
"no_nodes_found": "No Spacedrive nodes were found.", "no_nodes_found": "No Spacedrive nodes were found.",
@ -326,6 +336,7 @@
"online": "Online", "online": "Online",
"open": "Open", "open": "Open",
"open_file": "Open File", "open_file": "Open File",
"open_in_new_tab": "Open in new tab",
"open_new_location_once_added": "Open new location once added", "open_new_location_once_added": "Open new location once added",
"open_new_tab": "Open new tab", "open_new_tab": "Open new tab",
"open_object": "Open object", "open_object": "Open object",
@ -347,12 +358,14 @@
"pause": "Pause", "pause": "Pause",
"peers": "Peers", "peers": "Peers",
"people": "People", "people": "People",
"pin": "Pin",
"privacy": "Privacy", "privacy": "Privacy",
"privacy_description": "Spacedrive is built for privacy, that's why we're open source and local first. So we'll make it very clear what data is shared with us.", "privacy_description": "Spacedrive is built for privacy, that's why we're open source and local first. So we'll make it very clear what data is shared with us.",
"quick_preview": "Quick Preview", "quick_preview": "Quick Preview",
"quick_view": "Quick view", "quick_view": "Quick view",
"recent_jobs": "Recent Jobs", "recent_jobs": "Recent Jobs",
"recents": "Recents", "recents": "Recents",
"recents_notice_message": "Recents are created when you open a file.",
"regen_labels": "Regen Labels", "regen_labels": "Regen Labels",
"regen_thumbnails": "Regen Thumbnails", "regen_thumbnails": "Regen Thumbnails",
"regenerate_thumbs": "Regenerate Thumbs", "regenerate_thumbs": "Regenerate Thumbs",
@ -364,6 +377,7 @@
"rename": "Rename", "rename": "Rename",
"rename_object": "Rename object", "rename_object": "Rename object",
"replica": "Replica", "replica": "Replica",
"rescan": "Rescan",
"rescan_directory": "Rescan Directory", "rescan_directory": "Rescan Directory",
"rescan_location": "Rescan Location", "rescan_location": "Rescan Location",
"reset": "Reset", "reset": "Reset",
@ -377,6 +391,7 @@
"save": "Save", "save": "Save",
"save_changes": "Save Changes", "save_changes": "Save Changes",
"saved_searches": "Saved Searches", "saved_searches": "Saved Searches",
"search": "Search",
"search_extensions": "Search extensions", "search_extensions": "Search extensions",
"search_for_files_and_actions": "Search for files and actions...", "search_for_files_and_actions": "Search for files and actions...",
"secure_delete": "Secure delete", "secure_delete": "Secure delete",
@ -399,9 +414,9 @@
"show_slider": "Show slider", "show_slider": "Show slider",
"size": "Size", "size": "Size",
"size_b": "B", "size_b": "B",
"size_gb": "GB",
"size_kb": "kB", "size_kb": "kB",
"size_mb": "MB", "size_mb": "MB",
"size_gb": "GB",
"size_tb": "TB", "size_tb": "TB",
"skip_login": "Skip login", "skip_login": "Skip login",
"sort_by": "Sort by", "sort_by": "Sort by",
@ -429,9 +444,12 @@
"sync_with_library_description": "If enabled, your keybinds will be synced with library, otherwise they will apply only to this client.", "sync_with_library_description": "If enabled, your keybinds will be synced with library, otherwise they will apply only to this client.",
"tags": "Tags", "tags": "Tags",
"tags_description": "Manage your tags.", "tags_description": "Manage your tags.",
"tags_notice_message": "No items assigned to this tag.",
"telemetry_description": "Toggle ON to provide developers with detailed usage and telemetry data to enhance the app. Toggle OFF to send only basic data: your activity status, app version, core version, and platform (e.g., mobile, web, or desktop).", "telemetry_description": "Toggle ON to provide developers with detailed usage and telemetry data to enhance the app. Toggle OFF to send only basic data: your activity status, app version, core version, and platform (e.g., mobile, web, or desktop).",
"telemetry_title": "Share Additional Telemetry and Usage Data", "telemetry_title": "Share Additional Telemetry and Usage Data",
"temperature": "Temperature", "temperature": "Temperature",
"text_file": "Text File",
"text_size": "Text size",
"thank_you_for_your_feedback": "Thanks for your feedback!", "thank_you_for_your_feedback": "Thanks for your feedback!",
"thumbnailer_cpu_usage": "Thumbnailer CPU usage", "thumbnailer_cpu_usage": "Thumbnailer CPU usage",
"thumbnailer_cpu_usage_description": "Limit how much CPU the thumbnailer can use for background processing.", "thumbnailer_cpu_usage_description": "Limit how much CPU the thumbnailer can use for background processing.",
@ -462,21 +480,5 @@
"your_account": "Your account", "your_account": "Your account",
"your_account_description": "Spacedrive account and information.", "your_account_description": "Spacedrive account and information.",
"your_local_network": "Your Local Network", "your_local_network": "Your Local Network",
"your_privacy": "Your Privacy", "your_privacy": "Your Privacy"
"pin": "Pin", }
"rescan": "Rescan",
"open_in_new_tab": "Open in new tab",
"enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.",
"enable_networking_description_required": "Required for library sync or Spacedrop!",
"log_in": "Log in",
"logging_in": "Logging in...",
"no_favorite_items": "No favorite items",
"tags_notice_message": "No items assigned to this tag.",
"recents_notice_message": "Recents are created when you open a file.",
"no_items_found": "No items found",
"search": "Search",
"date_created": "Date Created",
"date_modified": "Date Modified",
"date_indexed": "Date Indexed",
"date_accessed": "Date Accessed"
}

View file

@ -104,7 +104,7 @@
"delete_rule": "Eliminar regla", "delete_rule": "Eliminar regla",
"delete_tag": "Eliminar Etiqueta", "delete_tag": "Eliminar Etiqueta",
"delete_tag_description": "¿Estás seguro de que quieres eliminar esta etiqueta? Esto no se puede deshacer y los archivos etiquetados serán desvinculados.", "delete_tag_description": "¿Estás seguro de que quieres eliminar esta etiqueta? Esto no se puede deshacer y los archivos etiquetados serán desvinculados.",
"delete_warning": "Advertencia: Esto eliminará tu {{type}} para siempre, aún no tenemos papelera...", "delete_warning": "Esto eliminará tu {{type}} para siempre, aún no tenemos papelera...",
"description": "Descripción", "description": "Descripción",
"deselect": "Deseleccionar", "deselect": "Deseleccionar",
"details": "Detalles", "details": "Detalles",

View file

@ -104,7 +104,7 @@
"delete_rule": "Supprimer la règle", "delete_rule": "Supprimer la règle",
"delete_tag": "Supprimer l'étiquette", "delete_tag": "Supprimer l'étiquette",
"delete_tag_description": "Êtes-vous sûr de vouloir supprimer cette étiquette ? Cela ne peut pas être annulé et les fichiers étiquetés seront dissociés.", "delete_tag_description": "Êtes-vous sûr de vouloir supprimer cette étiquette ? Cela ne peut pas être annulé et les fichiers étiquetés seront dissociés.",
"delete_warning": "Attention : Ceci supprimera votre {{type}} pour toujours, nous n'avons pas encore de corbeille...", "delete_warning": "Ceci supprimera votre {{type}} pour toujours, nous n'avons pas encore de corbeille...",
"description": "Description", "description": "Description",
"deselect": "Désélectionner", "deselect": "Désélectionner",
"details": "Détails", "details": "Détails",

View file

@ -104,7 +104,7 @@
"delete_rule": "Elimina regola", "delete_rule": "Elimina regola",
"delete_tag": "Elimina Tag", "delete_tag": "Elimina Tag",
"delete_tag_description": "Sei sicuro di voler cancellare questa etichetta? Questa operazione non può essere annullata e i file etichettati verranno scollegati.", "delete_tag_description": "Sei sicuro di voler cancellare questa etichetta? Questa operazione non può essere annullata e i file etichettati verranno scollegati.",
"delete_warning": "Attenzione: stai per eliminare il tuo {{type}} per sempre, non abbiamo ancora un cestino...", "delete_warning": "stai per eliminare il tuo {{type}} per sempre, non abbiamo ancora un cestino...",
"description": "Descrizione", "description": "Descrizione",
"deselect": "Deseleziona", "deselect": "Deseleziona",
"details": "Dettagli", "details": "Dettagli",

View file

@ -104,7 +104,7 @@
"delete_rule": "ルールを削除", "delete_rule": "ルールを削除",
"delete_tag": "タグを削除", "delete_tag": "タグを削除",
"delete_tag_description": "本当にこのタグを削除しますか?これを元に戻すことはできず、タグ付けされたファイル間の結びつきは失われます。", "delete_tag_description": "本当にこのタグを削除しますか?これを元に戻すことはできず、タグ付けされたファイル間の結びつきは失われます。",
"delete_warning": "【警告】これはあなたの {{type}} を完全に削除します。", "delete_warning": "これはあなたの {{type}} を完全に削除します。",
"description": "説明", "description": "説明",
"deselect": "クリップボードを空にする", "deselect": "クリップボードを空にする",
"details": "詳細", "details": "詳細",

View file

@ -104,7 +104,7 @@
"delete_rule": "Verwijder regel", "delete_rule": "Verwijder regel",
"delete_tag": "Verwijder Tag", "delete_tag": "Verwijder Tag",
"delete_tag_description": "Weet je zeker dat je deze tag wilt verwijderen? Dit kan niet ongedaan worden gemaakt en ge-tagde bestanden worden ontkoppeld.", "delete_tag_description": "Weet je zeker dat je deze tag wilt verwijderen? Dit kan niet ongedaan worden gemaakt en ge-tagde bestanden worden ontkoppeld.",
"delete_warning": "Waarschuwing: hiermee wordt je {{type}} permanent verwijderd, we hebben nog geen prullenbak...", "delete_warning": "hiermee wordt je {{type}} permanent verwijderd, we hebben nog geen prullenbak...",
"description": "Omschrijving", "description": "Omschrijving",
"deselect": "Deselecteer", "deselect": "Deselecteer",
"details": "Details", "details": "Details",

View file

@ -104,7 +104,7 @@
"delete_rule": "Удалить правило", "delete_rule": "Удалить правило",
"delete_tag": "Удалить тег", "delete_tag": "Удалить тег",
"delete_tag_description": "Вы уверены, что хотите удалить этот тег? Это действие нельзя отменить, и тегнутые файлы будут отсоединены.", "delete_tag_description": "Вы уверены, что хотите удалить этот тег? Это действие нельзя отменить, и тегнутые файлы будут отсоединены.",
"delete_warning": "Предупреждение: это удалит ваш {{type}} навсегда, у нас пока нет мусорной корзины.", "delete_warning": "это удалит ваш {{type}} навсегда, у нас пока нет мусорной корзины.",
"description": "Описание", "description": "Описание",
"deselect": "Отменить выбор", "deselect": "Отменить выбор",
"details": "Подробности", "details": "Подробности",

View file

@ -104,7 +104,7 @@
"delete_rule": "Kuralı Sil", "delete_rule": "Kuralı Sil",
"delete_tag": "Etiketi Sil", "delete_tag": "Etiketi Sil",
"delete_tag_description": "Bu etiketi silmek istediğinizden emin misiniz? Bu geri alınamaz ve etiketli dosyalar bağlantısız kalacak.", "delete_tag_description": "Bu etiketi silmek istediğinizden emin misiniz? Bu geri alınamaz ve etiketli dosyalar bağlantısız kalacak.",
"delete_warning": "Uyarı: Bu, {{type}}'ınızı sonsuza dek silecek, henüz çöp kutumuz yok...", "delete_warning": "Bu, {{type}}'ınızı sonsuza dek silecek, henüz çöp kutumuz yok...",
"description": "Açıklama", "description": "Açıklama",
"deselect": "Seçimi Kaldır", "deselect": "Seçimi Kaldır",
"details": "Detaylar", "details": "Detaylar",

View file

@ -104,7 +104,7 @@
"delete_rule": "删除规则", "delete_rule": "删除规则",
"delete_tag": "删除标签", "delete_tag": "删除标签",
"delete_tag_description": "您确定要删除这个标签吗?这不能被撤销,打过标签的文件将会被取消链接。", "delete_tag_description": "您确定要删除这个标签吗?这不能被撤销,打过标签的文件将会被取消链接。",
"delete_warning": "警告:这将永久删除您的{{type}},我们目前还没有回收站…", "delete_warning": "这将永久删除您的{{type}},我们目前还没有回收站…",
"description": "描述", "description": "描述",
"deselect": "取消选择", "deselect": "取消选择",
"details": "详情", "details": "详情",

View file

@ -104,7 +104,7 @@
"delete_rule": "刪除規則", "delete_rule": "刪除規則",
"delete_tag": "刪除標籤", "delete_tag": "刪除標籤",
"delete_tag_description": "您確定要刪除這個標籤嗎?這不能撤銷,並且帶有標籤的文件將被取消鏈接。", "delete_tag_description": "您確定要刪除這個標籤嗎?這不能撤銷,並且帶有標籤的文件將被取消鏈接。",
"delete_warning": "警告:這將永遠刪除您的{{type}},我們還沒有垃圾箱...", "delete_warning": "這將永遠刪除您的{{type}},我們還沒有垃圾箱...",
"description": "描述", "description": "描述",
"deselect": "取消選擇", "deselect": "取消選擇",
"details": "詳情", "details": "詳情",

View file

@ -73,6 +73,7 @@ export type Procedures = {
{ key: "ephemeralFiles.createFolder", input: LibraryArgs<CreateEphemeralFolderArgs>, result: string } | { key: "ephemeralFiles.createFolder", input: LibraryArgs<CreateEphemeralFolderArgs>, result: string } |
{ key: "ephemeralFiles.cutFiles", input: LibraryArgs<EphemeralFileSystemOps>, result: null } | { key: "ephemeralFiles.cutFiles", input: LibraryArgs<EphemeralFileSystemOps>, result: null } |
{ key: "ephemeralFiles.deleteFiles", input: LibraryArgs<string[]>, result: null } | { key: "ephemeralFiles.deleteFiles", input: LibraryArgs<string[]>, result: null } |
{ key: "ephemeralFiles.moveToTrash", input: LibraryArgs<string[]>, result: null } |
{ key: "ephemeralFiles.renameFile", input: LibraryArgs<EphemeralRenameFileArgs>, result: null } | { key: "ephemeralFiles.renameFile", input: LibraryArgs<EphemeralRenameFileArgs>, result: null } |
{ key: "files.convertImage", input: LibraryArgs<ConvertImageArgs>, result: null } | { key: "files.convertImage", input: LibraryArgs<ConvertImageArgs>, result: null } |
{ key: "files.copyFiles", input: LibraryArgs<OldFileCopierJobInit>, result: null } | { key: "files.copyFiles", input: LibraryArgs<OldFileCopierJobInit>, result: null } |
@ -81,6 +82,7 @@ export type Procedures = {
{ key: "files.cutFiles", input: LibraryArgs<OldFileCutterJobInit>, result: null } | { key: "files.cutFiles", input: LibraryArgs<OldFileCutterJobInit>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<OldFileDeleterJobInit>, result: null } | { key: "files.deleteFiles", input: LibraryArgs<OldFileDeleterJobInit>, result: null } |
{ key: "files.eraseFiles", input: LibraryArgs<OldFileEraserJobInit>, result: null } | { key: "files.eraseFiles", input: LibraryArgs<OldFileEraserJobInit>, result: null } |
{ key: "files.moveToTrash", input: LibraryArgs<OldFileDeleterJobInit>, result: null } |
{ key: "files.removeAccessTime", input: LibraryArgs<number[]>, result: null } | { key: "files.removeAccessTime", input: LibraryArgs<number[]>, result: null } |
{ key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } | { key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } |
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } | { key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |

View file

@ -121,7 +121,9 @@ export interface DialogProps<S extends FieldValues>
loading?: boolean; loading?: boolean;
trigger?: ReactNode; trigger?: ReactNode;
ctaLabel?: string; ctaLabel?: string;
ctaSecondLabel?: string;
onSubmit?: ReturnType<UseFormHandleSubmit<S>>; onSubmit?: ReturnType<UseFormHandleSubmit<S>>;
onSubmitSecond?: ReturnType<UseFormHandleSubmit<S>>;
children?: ReactNode; children?: ReactNode;
ctaDanger?: boolean; ctaDanger?: boolean;
closeLabel?: string; closeLabel?: string;
@ -141,6 +143,7 @@ export function Dialog<S extends FieldValues>({
form, form,
dialog, dialog,
onSubmit, onSubmit,
onSubmitSecond,
onCancelled = true, onCancelled = true,
invertButtonFocus, invertButtonFocus,
...props ...props
@ -190,7 +193,7 @@ export function Dialog<S extends FieldValues>({
) )
: !form.formState.isValid; : !form.formState.isValid;
const submitButton = ( const submitButton = !props.ctaSecondLabel ? (
<Button <Button
type="submit" type="submit"
size="sm" size="sm"
@ -200,9 +203,54 @@ export function Dialog<S extends FieldValues>({
props.ctaDanger && props.ctaDanger &&
'border-red-500 bg-red-500 focus:ring-1 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-app-selected' 'border-red-500 bg-red-500 focus:ring-1 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)} )}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
> >
{props.ctaLabel} {props.ctaLabel}
</Button> </Button>
) : (
<div className="flex flex-row gap-x-2">
<Button
type="submit"
size="sm"
disabled={form.formState.isSubmitting || props.submitDisabled || disableCheck}
variant={props.ctaDanger ? 'colored' : 'accent'}
className={clsx(
props.ctaDanger &&
'border-red-500 bg-red-500 focus:ring-1 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
>
{props.ctaLabel}
</Button>
<Button
type="submit"
size="sm"
disabled={form.formState.isSubmitting || props.submitDisabled || disableCheck}
variant={props.ctaDanger ? 'colored' : 'accent'}
className={clsx(
props.ctaDanger &&
'border-primary-500 bg-primary-500 focus:ring-1 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmitSecond?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
>
{props.ctaSecondLabel}
</Button>
</div>
); );
return ( return (
@ -226,8 +274,6 @@ export function Dialog<S extends FieldValues>({
form={form} form={form}
onSubmit={async (e) => { onSubmit={async (e) => {
e?.preventDefault(); e?.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false); setOpen(false);
}} }}
className={clsx( className={clsx(