From 3580fc15a92fc619c0f70bc318de5df5948b355d Mon Sep 17 00:00:00 2001 From: Justin Maximillian Kimlim Date: Fri, 26 Nov 2021 21:41:58 +0700 Subject: [PATCH 1/5] feat: search files/dirs inside a directory (#182) * feat: search files/folders in a dir using glob pattern * fix: hovering on search path make it showing the basename instead of full name * fix: broken label of the tab * feat: search in home dir * fix: opening dir is not working while searching file is in process. * feat: shortcut for search feature * fix: buggy search performance --- src-tauri/Cargo.lock | 7 ++ src-tauri/Cargo.toml | 1 + src-tauri/src/files_api.rs | 59 +++++++++- src-tauri/src/main.rs | 1 + src/Api/directory.ts | 28 +++++ src/Components/Files/File Operation/search.ts | 104 ++++++++++++++++++ src/Components/Functions/changePosition.ts | 6 +- src/Components/Layout/hover.ts | 4 +- src/Components/Layout/layout.scss | 11 ++ src/Components/Open/displayFiles.ts | 8 +- src/Components/Open/open.ts | 31 ++++++ src/Components/Shortcut/shortcut.ts | 7 ++ src/Components/Theme/theme.json | 8 ++ src/Components/Theme/theme.ts | 2 + src/index.html | 1 + src/index.ts | 3 + 16 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 src/Components/Files/File Operation/search.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e4be6b23..ede38e2c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -43,6 +43,7 @@ name = "app" version = "0.1.0" dependencies = [ "font-loader", + "glob", "lazy_static", "normpath", "notify", @@ -1248,6 +1249,12 @@ dependencies = [ "system-deps 3.2.0", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "globset" version = "0.4.8" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f787b18c..a21996f9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ notify = "4.0.17" normpath = "0.3.1" lazy_static = "1.4.0" font-loader = "0.11.0" +glob = "0.3.0" [features] default = [ "custom-protocol" ] diff --git a/src-tauri/src/files_api.rs b/src-tauri/src/files_api.rs index 1e56eae3..58f64a35 100644 --- a/src-tauri/src/files_api.rs +++ b/src-tauri/src/files_api.rs @@ -7,18 +7,21 @@ extern crate open; extern crate trash; use crate::file_lib; use crate::storage; +use glob::{glob_with, MatchOptions}; #[cfg(not(target_os = "macos"))] use normpath::PathExt; use notify::{raw_watcher, RawEvent, RecursiveMode, Watcher}; use serde_json::Value; use std::sync::mpsc::channel; +use tauri::api::path::local_data_dir; +use tauri::Manager; #[cfg(windows)] use std::os::windows::prelude::*; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Clone)] pub struct FileMetaData { file_path: String, basename: String, @@ -34,6 +37,7 @@ pub struct FileMetaData { created: SystemTime, is_trash: bool, } + #[derive(serde::Serialize)] pub struct TrashMetaData { file_path: String, @@ -531,7 +535,6 @@ pub async fn listen_dir(dir: String, window: tauri::Window) -> Result) -> u64 { } total_size } + +#[tauri::command] +pub async fn search_in_dir( + dir_path: String, + pattern: String, + window: tauri::Window, +) -> Vec { + let glob_pattern = match dir_path.as_ref() { + "xplorer://Home" => match cfg!(target_os = "windows") { + true => "C://**/".to_string() + &pattern, + false => "~/**/".to_string() + &pattern, + }, + _ => format!("{}/**/{}", dir_path, pattern), + }; + let glob_option = MatchOptions { + case_sensitive: false, + require_literal_separator: false, + require_literal_leading_dot: false, + ..Default::default() + }; + let continue_search = std::sync::Arc::new(std::sync::Mutex::new(true)); + let id = window.listen("unsearch", { + let continue_search = continue_search.clone(); + move |_| { + *continue_search.lock().unwrap() = false; + } + }); + let mut files = Vec::new(); + let glob_result = glob_with(&glob_pattern, glob_option).unwrap(); + for entry in glob_result { + if continue_search.lock().unwrap().clone() { + match entry { + Ok(path) => { + files.push( + get_file_properties(path.to_str().unwrap().to_string()) + .await + .unwrap(), + ); + if files.len() % 100 == 0 { + window.emit("search_partial_result", files.clone()).unwrap(); + files.clear(); + } + } + Err(e) => println!("{:?}", e), + } + } else { + break; + } + } + window.unlisten(id); + files +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 779f6c12..526e2512 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -107,6 +107,7 @@ fn main() { files_api::get_file_properties, files_api::extract_icon, files_api::calculate_files_total_size, + files_api::search_in_dir, drives::get_drives, storage::write_data, storage::read_data, diff --git a/src/Api/directory.ts b/src/Api/directory.ts index fd767ade..a05a12a3 100644 --- a/src/Api/directory.ts +++ b/src/Api/directory.ts @@ -5,6 +5,7 @@ import type FileMetaData from '../Typings/fileMetaData'; import { getCurrent } from '@tauri-apps/api/window'; import { UnlistenFn } from '@tauri-apps/api/event'; let listener: UnlistenFn; +let searchListener: UnlistenFn; interface DirectoryData { files: FileMetaData[]; number_of_files: number; @@ -89,6 +90,33 @@ class DirectoryAPI { async getSize(): Promise { return await invoke('get_dir_size', { dir: this.dirName }); } + + /** + * Stop all searching progress + * @returns {Promise} + */ + async stopSearching(): Promise { + const listenerExist = searchListener !== null && searchListener !== undefined; + searchListener?.(); + await getCurrent().emit('unsearch'); + return listenerExist; + } + + /** + * Search for a file/folder in a directory + * @param {string} pattern - glob pattern + * @param {FileMetaData[] => void} callback - progress callback + * @returns {any} + */ + async search(pattern: string, callback: (partialFound: FileMetaData[]) => void): Promise { + searchListener = await getCurrent().listen('search_partial_result', (res) => { + if (searchListener !== null && searchListener !== undefined) callback(res.payload as FileMetaData[]); + }); + const res = await invoke('search_in_dir', { dirPath: this.dirName, pattern }); + await this.stopSearching(); + searchListener = null; + return res as FileMetaData[]; + } } export default DirectoryAPI; diff --git a/src/Components/Files/File Operation/search.ts b/src/Components/Files/File Operation/search.ts new file mode 100644 index 00000000..6a514967 --- /dev/null +++ b/src/Components/Files/File Operation/search.ts @@ -0,0 +1,104 @@ +import focusingPath from '../../Functions/focusingPath'; +import DirectoryAPI from '../../../Api/directory'; +import { startLoading, stopLoading } from '../../Functions/Loading/loading'; +import displayFiles from '../../Open/displayFiles'; +import changePosition from '../../Functions/changePosition'; +import { LOAD_IMAGE } from '../../Functions/lazyLoadingImage'; +import { updateTheme } from '../../Theme/theme'; +import { OpenLog } from '../../Functions/log'; +import { OpenDir } from '../../Open/open'; +let being_watch: string; + +const stopSearchingProcess = async (): Promise => { + being_watch = null; + if (await new DirectoryAPI('').stopSearching()) stopLoading(); +}; + +/** + * Process searching + * @param {string} to_search - glob pattern to search + * @param {string} search_in - dir to search into + * @returns {Promise} + */ +const processSearch = async (to_search: string, search_in: string): Promise => { + const MAIN_ELEMENT = document.getElementById('workspace'); + MAIN_ELEMENT.innerHTML = ''; + if (!to_search.length) OpenDir(search_in); + startLoading(); + const search_path = `Search: [[${to_search}]] inside [[${search_in}]]`; + changePosition(search_path); + let foundSomething = false; + + const finalResult = await new DirectoryAPI(search_in).search(to_search, async (partialFound) => { + let _el = document.createElement('div') as HTMLElement; + foundSomething = true; + if (document.querySelector('.path-navigator').value.startsWith('Search: ')) + _el = await displayFiles(partialFound, search_path, _el, null, true); + for (const childEl of _el.children) { + MAIN_ELEMENT.appendChild(childEl); + } + }); + const _el = document.createElement('div'); + MAIN_ELEMENT.appendChild(await displayFiles(finalResult, search_path, _el, null, true)); + if (!finalResult.length && !foundSomething) { + MAIN_ELEMENT.classList.add('empty-dir-notification'); + MAIN_ELEMENT.innerText = "Can't find specified query"; + } + stopLoading(); + updateTheme('grid'); + LOAD_IMAGE(); + OpenLog(search_path); +}; + +/** + * Get the real focusing path + * @returns {Promise} + */ +const getFocusingPath = async (): Promise => { + let _focusingPath = await focusingPath(); + if (_focusingPath.startsWith('Search: ')) { + const splitByInsideKeyword = _focusingPath.split(' inside '); + if (splitByInsideKeyword.length === 2) { + _focusingPath = splitByInsideKeyword[1].slice(2, -2); + } else { + for (let i = 0; i < splitByInsideKeyword.length; i++) { + if (splitByInsideKeyword[i]?.endsWith(']]') && splitByInsideKeyword[i + 1]?.startsWith('[[')) { + _focusingPath = splitByInsideKeyword + .slice(i + 1) + .join(' inside ') + .slice(2, -2); + } + } + } + } + return _focusingPath; +}; +/** + * Initialize search feature in Xplorer + * @returns {Promise} + */ +const Search = async (): Promise => { + let listener: ReturnType; + document.querySelector('.search-bar').addEventListener('keydown', async (e: KeyboardEvent) => { + clearTimeout(listener); + if (e.ctrlKey && e.key === 'f') { + return; + } else if (e.key === 'Enter') { + const value = (e.target as HTMLInputElement).value; + if (value !== being_watch) { + processSearch(value, await getFocusingPath()); + being_watch = value; + } + } else { + listener = setTimeout(async () => { + const value = (e.target as HTMLInputElement).value; + if (value !== being_watch) { + processSearch(value, await getFocusingPath()); + being_watch = value; + } + }, 1000); + } + }); +}; +export default Search; +export { processSearch, stopSearchingProcess }; diff --git a/src/Components/Functions/changePosition.ts b/src/Components/Functions/changePosition.ts index 655a23dc..53367a2f 100644 --- a/src/Components/Functions/changePosition.ts +++ b/src/Components/Functions/changePosition.ts @@ -31,7 +31,11 @@ const changePosition = async (newPath: string, forceChange = false): Promise('#tab-position').innerText = await Translate( - basename(newPath) === '' ? newPath : basename(newPath) + document.querySelector('.path-navigator').value.startsWith('Search: ') + ? newPath + : basename(newPath) === '' + ? newPath + : basename(newPath) ); Storage.set(`tabs-${windowName}`, tabs); changeSelectedAllStatus(); diff --git a/src/Components/Layout/hover.ts b/src/Components/Layout/hover.ts index 2565dccf..74cfa161 100644 --- a/src/Components/Layout/hover.ts +++ b/src/Components/Layout/hover.ts @@ -32,6 +32,8 @@ const Hover = (): void => { if (hoveringElement?.dataset?.path && displayName) hoveringElement.querySelector('.file-grid-filename').innerHTML = displayName; return; } + const isOnSearch = document.querySelector('.path-navigator').value.startsWith('Search: '); + hoveringElement?.classList?.remove('hovering'); const target = (e.target as HTMLElement)?.dataset?.path ? (e.target as HTMLElement) : ((e.target as HTMLElement)?.parentNode as HTMLElement); @@ -48,7 +50,7 @@ const Hover = (): void => { timeOut = window.setTimeout(async () => { displayName = filenameGrid.innerHTML; const path = unescape(target.dataset.path); - filenameGrid.innerHTML = getBasename(path); + filenameGrid.innerHTML = isOnSearch ? path : getBasename(path); target?.classList?.add('hovering'); const previewImageOnHover = (await Storage.get('appearance')).previewImageOnHover ?? true; diff --git a/src/Components/Layout/layout.scss b/src/Components/Layout/layout.scss index 3aedd149..937820ca 100644 --- a/src/Components/Layout/layout.scss +++ b/src/Components/Layout/layout.scss @@ -209,12 +209,23 @@ } } .path-navigator { + flex: 3; + margin: 0 1rem; + border: none; + border-radius: 5px; + padding: 0.5rem; + vertical-align: middle; + } + .search-bar { flex: 1; margin: 0 1rem; border: none; border-radius: 5px; padding: 0.5rem; vertical-align: middle; + &::-webkit-input-placeholder { + color: inherit; + } } } } diff --git a/src/Components/Open/displayFiles.ts b/src/Components/Open/displayFiles.ts index 3eddd73c..9c70aa8d 100644 --- a/src/Components/Open/displayFiles.ts +++ b/src/Components/Open/displayFiles.ts @@ -12,13 +12,17 @@ import getDefaultSort from './defaultSort'; * @param {fileData[]} files - array of files of a directory * @param {string} dir - directory base path * @param {HTMLElement} onElement - element to append files element + * @param {{reveal: boolean, revealDir: boolean}} options - options + * @param {boolean} isSearch - if true, files are searched + * * @returns {Promise} */ const displayFiles = async ( files: FileMetaData[], dir: string, onElement?: HTMLElement, - options?: { reveal: boolean; revealDir: string } + options?: { reveal: boolean; revealDir: string }, + isSearch?: boolean ): Promise => { const FilesElement = onElement ?? document.createElement('div'); const preference = await Storage.get('preference'); @@ -76,6 +80,7 @@ const displayFiles = async ( displayName = file.basename.length > 20 ? file.basename.substring(0, 20) + '...' : file.basename; break; } + if (isSearch) displayName = file.file_path; fileGrid.setAttribute('draggable', 'true'); if (!file.is_trash) { @@ -120,7 +125,6 @@ const displayFiles = async ( Select(document.querySelector(`.file[data-path="${escape(options?.revealDir)}"]`), false, false); } - OpenLog(dir); return FilesElement; }; diff --git a/src/Components/Open/open.ts b/src/Components/Open/open.ts index 7527558b..eb4440b1 100644 --- a/src/Components/Open/open.ts +++ b/src/Components/Open/open.ts @@ -18,6 +18,7 @@ import focusingPath from '../Functions/focusingPath'; import { LOAD_IMAGE } from '../Functions/lazyLoadingImage'; import PromptError from '../Prompt/error'; import { UpdateInfo } from '../Layout/infobar'; +import { processSearch, stopSearchingProcess } from '../Files/File Operation/search'; let platform: string; let directoryInfo: DirectoryAPI; /** @@ -28,6 +29,7 @@ let directoryInfo: DirectoryAPI; * @returns {Promise} */ const OpenDir = async (dir: string, reveal?: boolean, forceOpen = false): Promise => { + await stopSearchingProcess(); if (isLoading() && !forceOpen) { InfoLog(`Something is still loading, refusing to open dir ${dir}`); return; @@ -44,6 +46,7 @@ const OpenDir = async (dir: string, reveal?: boolean, forceOpen = false): Promis if (dir === 'xplorer://Home') { Home(); UpdateInfo('number-of-files', ''); + OpenLog(dir); } else if (dir === 'xplorer://Trash') { if (!platform) platform = await OS(); if (platform === 'darwin') { @@ -66,9 +69,36 @@ const OpenDir = async (dir: string, reveal?: boolean, forceOpen = false): Promis } }); } + OpenLog(dir); } else if (dir === 'xplorer://Recent') { Recent(); UpdateInfo('number-of-files', ''); + OpenLog(dir); + } else if (dir.startsWith('Search')) { + // Search path pattern: Search: [[search-query]] inside [[search-path]] + const splitBySearchKeyword = dir.split('Search: '); + splitBySearchKeyword.shift(); + const query = splitBySearchKeyword.join('Search: '); + const splitByInsideKeyword = query.split(' inside '); + if (splitByInsideKeyword.length === 2) { + const searchQuery = splitByInsideKeyword[0].slice(2, -2); + const searchPath = splitByInsideKeyword[1].slice(2, -2); + processSearch(searchQuery, searchPath); + } else { + for (let i = 0; i < splitByInsideKeyword.length; i++) { + if (splitByInsideKeyword[i]?.endsWith(']]') && splitByInsideKeyword[i + 1]?.startsWith('[[')) { + const searchQuery = splitByInsideKeyword + .slice(0, i + 1) + .join(' inside ') + .slice(2, -2); + const searchPath = splitByInsideKeyword + .slice(i + 1) + .join(' inside ') + .slice(2, -2); + processSearch(searchQuery, searchPath); + } + } + } } else { if (reveal) { directoryInfo = new DirectoryAPI(getDirname(dir)); @@ -120,6 +150,7 @@ const OpenDir = async (dir: string, reveal?: boolean, forceOpen = false): Promis return; } } + OpenLog(dir); } }; /** diff --git a/src/Components/Shortcut/shortcut.ts b/src/Components/Shortcut/shortcut.ts index 2396094d..de95ca31 100644 --- a/src/Components/Shortcut/shortcut.ts +++ b/src/Components/Shortcut/shortcut.ts @@ -230,6 +230,13 @@ const Shortcut = (): void => { const selectedFilePath = unescape(selectedFile?.dataset?.path); Properties(selectedFilePath === 'undefined' ? await focusingPath() : selectedFilePath); } + // Find files (Ctrl+F) + else if (e.ctrlKey && e.key === 'f') { + e.preventDefault(); + const searchElement = document.querySelector('.search-bar'); + searchElement.select(); + searchElement.focus(); + } // Internal Reload (F5) if (e.key === 'F5') { e.preventDefault(); diff --git a/src/Components/Theme/theme.json b/src/Components/Theme/theme.json index 1cab8566..40e84046 100644 --- a/src/Components/Theme/theme.json +++ b/src/Components/Theme/theme.json @@ -30,6 +30,8 @@ "navigatorColor": "#4fe0be", "pathNavigatorBackground": "#5f6b94", "pathNavigatorColor": "#a2b7ff", + "searchBarBackground": "#5f6b94", + "searchBarColor": "#a2b7ff", "settingsSidebarBackground": "#14141b", "settingsMainBackground": "#1a1b26", @@ -124,6 +126,8 @@ "navigatorColor": "#87c1ce", "pathNavigatorBackground": "#39496a", "pathNavigatorColor": "#87c1ce", + "searchBarBackground": "#39496a", + "searchBarColor": "#87c1ce", "settingsSidebarBackground": "#1b1e2e", "settingsMainBackground": "#24283b", @@ -218,6 +222,8 @@ "navigatorColor": "#202124", "pathNavigatorBackground": "#e0e0e0", "pathNavigatorColor": "#202124", + "searchBarBackground": "#e0e0e0", + "searchBarColor": "inherit", "settingsSidebarBackground": "#cbccd1", "settingsMainBackground": "#d5d6db", @@ -312,6 +318,8 @@ "navigatorColor": "#202124", "pathNavigatorBackground": "#dfdfdf", "pathNavigatorColor": "#202124", + "searchBarBackground": "#ebebeb", + "searchBarColor": "inherit", "settingsSidebarBackground": "#FAFAFA", "settingsMainBackground": "#FEFEFE", diff --git a/src/Components/Theme/theme.ts b/src/Components/Theme/theme.ts index 4f965fda..88973893 100644 --- a/src/Components/Theme/theme.ts +++ b/src/Components/Theme/theme.ts @@ -177,6 +177,8 @@ const changeTheme = async ( if (category === '*' || category === 'tabbing') { changeElementTheme(document.querySelector('.path-navigator'), 'pathNavigatorBackground', 'background', theme); changeElementTheme(document.querySelector('.path-navigator'), 'pathNavigatorColor', 'color', theme); + changeElementTheme(document.querySelector('.search-bar'), 'searchBarBackground', 'background', theme); + changeElementTheme(document.querySelector('.search-bar'), 'searchBarColor', 'color', theme); document .querySelector('.tabs-manager') .style.setProperty('--tabs-scrollbar-track', themeJSON ? themeJSON.tabsScrollbarTrack : defaultThemeJSON[theme]?.tabsScrollbarTrack); diff --git a/src/index.html b/src/index.html index b49a8f10..380142e7 100644 --- a/src/index.html +++ b/src/index.html @@ -38,6 +38,7 @@ +
diff --git a/src/index.ts b/src/index.ts index 3cc99fc9..c511b77b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import ContextMenu from './Components/ContextMenu/contextMenu'; import Hover from './Components/Layout/hover'; import LAZY_LOAD_INIT from './Components/Functions/lazyLoadingImage'; import Infobar from './Components/Layout/infobar'; +import Search from './Components/Files/File Operation/search'; // Wait DOM Loaded to be loaded document.addEventListener('DOMContentLoaded', async () => { // Read user preferences @@ -64,6 +65,8 @@ document.addEventListener('DOMContentLoaded', async () => { ContextMenu(); // Initialize hover handler Hover(); + // Initialize search feature + Search(); // Initialize lazy loading image handler (for performance) LAZY_LOAD_INIT(); }); From 3f21ce7a875e1444dbf2163e7803772ea36ff6f7 Mon Sep 17 00:00:00 2001 From: Justin Maximillian Kimlim Date: Sat, 27 Nov 2021 14:50:36 +0700 Subject: [PATCH 2/5] fix: xplorer crashes on deleting files from trash (resolve #183) --- src/Components/Shortcut/shortcut.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Shortcut/shortcut.ts b/src/Components/Shortcut/shortcut.ts index de95ca31..2baa5e05 100644 --- a/src/Components/Shortcut/shortcut.ts +++ b/src/Components/Shortcut/shortcut.ts @@ -196,7 +196,7 @@ const Shortcut = (): void => { if (e.shiftKey) { const filePaths = []; for (const element of getSelected()) { - filePaths.push(_focusingPath === 'xplorer://Trash' ? unescape(element.dataset.realPath) : unescape(element.dataset.path)); + filePaths.push(unescape(element.dataset.path)); } if (_focusingPath === 'xplorer://Trash') Purge(filePaths); else PermanentDelete(filePaths); From cf140a08217719b59a30f818c5240ed2e26e0aa8 Mon Sep 17 00:00:00 2001 From: Justin Maximillian Kimlim Date: Sat, 27 Nov 2021 15:51:32 +0700 Subject: [PATCH 3/5] fix: typo on context menu and fix missing icon --- src/Components/ContextMenu/configs/bodyMenu.config.ts | 2 ++ .../ContextMenu/configs/multipleSelectedMenu.config.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Components/ContextMenu/configs/bodyMenu.config.ts b/src/Components/ContextMenu/configs/bodyMenu.config.ts index 17dc73d7..4ba0e6cf 100644 --- a/src/Components/ContextMenu/configs/bodyMenu.config.ts +++ b/src/Components/ContextMenu/configs/bodyMenu.config.ts @@ -168,6 +168,7 @@ const BodyMenu = async (target: HTMLElement, filePath: string): Promise { const filePaths = [...document.querySelectorAll('.file')].map((file) => unescape(file.dataset.path)); Restore(filePaths); @@ -176,6 +177,7 @@ const BodyMenu = async (target: HTMLElement, filePath: string): Promise { const filePaths = [...document.querySelectorAll('.file')].map((file) => unescape(file.dataset.path)); Purge(filePaths); diff --git a/src/Components/ContextMenu/configs/multipleSelectedMenu.config.ts b/src/Components/ContextMenu/configs/multipleSelectedMenu.config.ts index bf1ba77d..a186cc72 100644 --- a/src/Components/ContextMenu/configs/multipleSelectedMenu.config.ts +++ b/src/Components/ContextMenu/configs/multipleSelectedMenu.config.ts @@ -80,6 +80,7 @@ const MultipleSelectedMenu = async (_: HTMLElement, _filePath: string): Promise< { menu: await Translate('Restore these files'), visible: _focusingPath === 'xplorer://Trash', + icon: 'delete', role: () => { const filePaths = []; for (const element of getSelected()) { @@ -89,8 +90,9 @@ const MultipleSelectedMenu = async (_: HTMLElement, _filePath: string): Promise< }, }, { - menu: await Translate('Permanently these files'), + menu: await Translate('Permanently delete these files'), visible: _focusingPath === 'xplorer://Trash', + icon: 'delete', role: () => { const filePaths = []; for (const element of getSelected()) { From b283400d8fabd9a1cd7804bb326313f8dd15efa0 Mon Sep 17 00:00:00 2001 From: Justin Maximillian Kimlim Date: Sun, 28 Nov 2021 20:02:02 +0700 Subject: [PATCH 4/5] fix: original name prompted out not showing the whole basename if there's space in the basename --- src/Components/Files/File Operation/rename.ts | 36 +++++++------------ src/Components/Prompt/ask.ts | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/Components/Files/File Operation/rename.ts b/src/Components/Files/File Operation/rename.ts index 0aafacea..037d7d3d 100644 --- a/src/Components/Files/File Operation/rename.ts +++ b/src/Components/Files/File Operation/rename.ts @@ -14,31 +14,19 @@ import ConfirmDialog from '../../Prompt/confirm'; * @returns {void} */ const Rename = (filePath: string): void => { - Ask('Rename', 'New File Name:', { value: basename(filePath) }).then( - async (newName: string) => { - const target = - getDirname(newName) === '.' - ? joinPath(await focusingPath(), newName) - : joinPath(getDirname(filePath), newName); - if (await new FileAPI(target).exists()) { - const confirm = await ConfirmDialog( - 'File Exists', - 'The new name already exists, do you want to overwrite it?', - 'No' - ); - if (!confirm) return; - } - try { - new OperationAPI(filePath, target).rename(); - } catch (err) { - PromptError( - 'Error renaming file', - `Failed to rename ${filePath} [${err}]` - ); - } - OperationLog('rename', unescape(filePath), target); + Ask('Rename', 'New File Name:', { value: basename(filePath) }).then(async (newName: string) => { + const target = getDirname(newName) === '.' ? joinPath(await focusingPath(), newName) : joinPath(getDirname(filePath), newName); + if (await new FileAPI(target).exists()) { + const confirm = await ConfirmDialog('File Exists', 'The new name already exists, do you want to overwrite it?', 'No'); + if (!confirm) return; } - ); + try { + new OperationAPI(filePath, target).rename(); + } catch (err) { + PromptError('Error renaming file', `Failed to rename ${filePath} [${err}]`); + } + OperationLog('rename', unescape(filePath), target); + }); }; export default Rename; diff --git a/src/Components/Prompt/ask.ts b/src/Components/Prompt/ask.ts index 198f26ea..5b4a1a31 100644 --- a/src/Components/Prompt/ask.ts +++ b/src/Components/Prompt/ask.ts @@ -19,7 +19,7 @@ const Ask = async (title: string, message: string, options?: AskOptions): Promis
${message ? `
${message}
` : ''} - +
From 73a856a05c02afcf0d66f6174c623e41cd97f800 Mon Sep 17 00:00:00 2001 From: Justin Maximillian Kimlim Date: Sun, 28 Nov 2021 20:09:50 +0700 Subject: [PATCH 5/5] feat: set selection range as the file name without the extension when renaming file --- src/Components/Prompt/ask.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/Prompt/ask.ts b/src/Components/Prompt/ask.ts index 5b4a1a31..f6806163 100644 --- a/src/Components/Prompt/ask.ts +++ b/src/Components/Prompt/ask.ts @@ -1,5 +1,6 @@ import { updateTheme } from '../Theme/theme'; import dragElement from '../Functions/dragElement'; + interface AskOptions { value: string; } @@ -32,8 +33,8 @@ const Ask = async (title: string, message: string, options?: AskOptions): Promis updateTheme('prompt'); const promptInput = promptElement.querySelector('.prompt-input'); + promptInput.setSelectionRange(0, promptInput.value.lastIndexOf('.')); promptInput.focus(); - promptInput.select(); return new Promise((resolve) => { promptElement.querySelector('.prompt-ok').addEventListener('click', () => { promptElement.parentNode.removeChild(promptElement);