diff --git a/src/UI/UIDesktop.js b/src/UI/UIDesktop.js index 030546e5f..2926f433d 100644 --- a/src/UI/UIDesktop.js +++ b/src/UI/UIDesktop.js @@ -726,6 +726,16 @@ async function UIDesktop(options){ } }, // ------------------------------------------- + // Undo + // ------------------------------------------- + { + html: "Undo", + disabled: actions_history.length > 0 ? false : true, + onClick: function(){ + undo_last_action(); + } + }, + // ------------------------------------------- // Upload Here // ------------------------------------------- { diff --git a/src/UI/UIItem.js b/src/UI/UIItem.js index 4d5801285..e90f420f9 100644 --- a/src/UI/UIItem.js +++ b/src/UI/UIItem.js @@ -627,104 +627,7 @@ function UIItem(options){ $(el_item_name_editor).removeClass('item-name-editor-active'); // Perform rename request - puter.fs.rename({ - uid: options.uid === 'null' ? null : options.uid, - new_name: new_name, - excludeSocketID: window.socket.id, - success: async (fsentry)=>{ - // Has the extension changed? in that case update options.sugggested_apps - const old_extension = path.extname(old_name); - const new_extension = path.extname(new_name); - if(old_extension !== new_extension){ - suggest_apps_for_fsentry({ - uid: options.uid, - onSuccess: function(suggested_apps){ - options.suggested_apps = suggested_apps; - } - }); - } - - // Set new item name - $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).html(html_encode(truncate_filename(new_name, TRUNCATE_LENGTH)).replaceAll(' ', ' ')); - $(el_item_name).show(); - - // Hide item name editor - $(el_item_name_editor).hide(); - - // Set new icon - const new_icon = (options.is_dir ? window.icons['folder.svg'] : (await item_icon(fsentry)).image); - $(el_item_icon).find('.item-icon-icon').attr('src', new_icon); - - // Set new data-name - options.name = new_name; - $(el_item).attr('data-name', html_encode(new_name)); - $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-name', html_encode(new_name)); - $(`.window-${options.uid}`).attr('data-name', html_encode(new_name)); - - // Set new title attribute - $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('title', html_encode(new_name)); - $(`.window-${options.uid}`).attr('title', html_encode(new_name)); - - // Set new value for item-name-editor - $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name-editor`).val(html_encode(new_name)); - $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).attr('title', html_encode(new_name)); - - // Set new data-path - options.path = path.join( path.dirname(options.path), options.name); - const new_path = options.path; - $(el_item).attr('data-path', new_path); - $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path); - $(`.window-${options.uid}`).attr('data-path', new_path); - - // Update all elements that have matching paths - $(`[data-path="${html_encode(old_path)}" i]`).each(function(){ - $(this).attr('data-path', new_path) - if($(this).hasClass('window-navbar-path-dirname')) - $(this).text(new_name); - }); - - // Update the paths of all elements whose paths start with old_path - $(`[data-path^="${html_encode(old_path) + '/'}"]`).each(function(){ - const new_el_path = _.replace($(this).attr('data-path'), old_path + '/', new_path+'/'); - $(this).attr('data-path', new_el_path); - }); - - // Update the 'Sites Cache' - if($(el_item).attr('data-has_website') === '1') - await update_sites_cache(); - - // Update website_url - website_url = determine_website_url(new_path); - $(el_item).attr('data-website_url', website_url); - - // Update all exact-matching windows - $(`.window-${options.uid}`).each(function(){ - update_window_path(this, options.path); - }) - - // Set new name for corresponding open windows - $(`.window-${options.uid} .window-head-title`).text(new_name); - - // Re-sort all matching item containers - $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).parent('.item-container').each(function(){ - sort_items(this, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order')); - }) - }, - error: function (err){ - // reset to old name - $(el_item_name).text(truncate_filename(options.name, TRUNCATE_LENGTH)); - $(el_item_name).show(); - - // hide item name editor - $(el_item_name_editor).hide(); - $(el_item_name_editor).val(html_encode($(el_item).attr('data-name'))); - - //show error - if(err.message){ - UIAlert(err.message) - } - }, - }); + rename_file(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url); } // -------------------------------------------------------- diff --git a/src/UI/UIWindow.js b/src/UI/UIWindow.js index 8d5dc5090..3a90926fe 100644 --- a/src/UI/UIWindow.js +++ b/src/UI/UIWindow.js @@ -1917,6 +1917,16 @@ async function UIWindow(options) { } }, // ------------------------------------------- + // Undo + // ------------------------------------------- + { + html: "Undo", + disabled: actions_history.length > 0 ? false : true, + onClick: function(){ + undo_last_action(); + } + }, + // ------------------------------------------- // Upload Here // ------------------------------------------- { diff --git a/src/globals.js b/src/globals.js index d2d336e0e..f5ff998ad 100644 --- a/src/globals.js +++ b/src/globals.js @@ -19,6 +19,7 @@ window.clipboard_op = ''; window.clipboard = []; +window.actions_history = []; window.window_nav_history = {}; window.window_nav_history_current_position = {}; window.progress_tracker = []; diff --git a/src/helpers.js b/src/helpers.js index 9e38f7701..a12fb405a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1434,9 +1434,15 @@ window.create_folder = async(basedir, appendto_element)=>{ overwrite: false, success: function (data){ const el_created_dir = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]'); - if(el_created_dir.length > 0) + if(el_created_dir.length > 0){ activate_item_name_editor(el_created_dir); + // Add action to actions_history for undo ability + actions_history.push({ + operation: 'create_folder', + data: el_created_dir + }); + } clearTimeout(progwin_timeout); // done @@ -1472,6 +1478,12 @@ window.create_file = async(options)=>{ const created_file = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]'); if(created_file.length > 0){ activate_item_name_editor(created_file); + + // Add action to actions_history for undo ability + actions_history.push({ + operation: 'create_file', + data: created_file + }); } } }); @@ -1515,6 +1527,8 @@ window.copy_clipboard_items = async function(dest_path, dest_container_element){ progwin = await UIWindowCopyProgress({operation_id: copy_op_id}); }, 2000); + const copied_item_paths = [] + for(let i=0; i} */ -window.move_items = async function(el_items, dest_path){ +window.move_items = async function(el_items, dest_path, is_undo = false){ let move_op_id = operation_id++; operation_cancelled[move_op_id] = false; @@ -2304,6 +2339,9 @@ window.move_items = async function(el_items, dest_path){ progwin = await UIWindowMoveProgress({operation_id: move_op_id}); }, 2000); + // storing moved items for undo ability + const moved_items = [] + // Go through each item and try to move it for(let i=0; i { $(progwin).close(); @@ -2936,6 +2989,20 @@ window.upload_items = async function(items, dest_path){ // success success: async function(items){ // DONE + // Add action to actions_history for undo ability + const files = [] + if(typeof items[Symbol.iterator] === 'function'){ + for(const item of items){ + files.push(item.path) + } + }else{ + files.push(items.path) + } + + actions_history.push({ + operation: 'upload', + data: files + }); // close progress window after a bit of delay for a better UX setTimeout(() => { setTimeout(() => { @@ -3318,3 +3385,314 @@ window.unzipItem = async function(itemPath) { }, Math.max(0, copy_progress_hide_delay - (Date.now() - start_ts))); }) } + +window.rename_file = async(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, is_undo = false)=>{ + puter.fs.rename({ + uid: options.uid === 'null' ? null : options.uid, + new_name: new_name, + excludeSocketID: window.socket.id, + success: async (fsentry)=>{ + // Add action to actions_history for undo ability + if (!is_undo) + actions_history.push({ + operation: 'rename', + data: {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url} + }); + + // Has the extension changed? in that case update options.sugggested_apps + const old_extension = path.extname(old_name); + const new_extension = path.extname(new_name); + if(old_extension !== new_extension){ + suggest_apps_for_fsentry({ + uid: options.uid, + onSuccess: function(suggested_apps){ + options.suggested_apps = suggested_apps; + } + }); + } + + // Set new item name + $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).html(html_encode(truncate_filename(new_name, TRUNCATE_LENGTH)).replaceAll(' ', ' ')); + $(el_item_name).show(); + + // Hide item name editor + $(el_item_name_editor).hide(); + + // Set new icon + const new_icon = (options.is_dir ? window.icons['folder.svg'] : (await item_icon(fsentry)).image); + $(el_item_icon).find('.item-icon-icon').attr('src', new_icon); + + // Set new data-name + options.name = new_name; + $(el_item).attr('data-name', html_encode(new_name)); + $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-name', html_encode(new_name)); + $(`.window-${options.uid}`).attr('data-name', html_encode(new_name)); + + // Set new title attribute + $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('title', html_encode(new_name)); + $(`.window-${options.uid}`).attr('title', html_encode(new_name)); + + // Set new value for item-name-editor + $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name-editor`).val(html_encode(new_name)); + $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).attr('title', html_encode(new_name)); + + // Set new data-path + options.path = path.join( path.dirname(options.path), options.name); + const new_path = options.path; + $(el_item).attr('data-path', new_path); + $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path); + $(`.window-${options.uid}`).attr('data-path', new_path); + + // Update all elements that have matching paths + $(`[data-path="${html_encode(old_path)}" i]`).each(function(){ + $(this).attr('data-path', new_path) + if($(this).hasClass('window-navbar-path-dirname')) + $(this).text(new_name); + }); + + // Update the paths of all elements whose paths start with old_path + $(`[data-path^="${html_encode(old_path) + '/'}"]`).each(function(){ + const new_el_path = _.replace($(this).attr('data-path'), old_path + '/', new_path+'/'); + $(this).attr('data-path', new_el_path); + }); + + // Update the 'Sites Cache' + if($(el_item).attr('data-has_website') === '1') + await update_sites_cache(); + + // Update website_url + website_url = determine_website_url(new_path); + $(el_item).attr('data-website_url', website_url); + + // Update all exact-matching windows + $(`.window-${options.uid}`).each(function(){ + update_window_path(this, options.path); + }) + + // Set new name for corresponding open windows + $(`.window-${options.uid} .window-head-title`).text(new_name); + + // Re-sort all matching item containers + $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).parent('.item-container').each(function(){ + sort_items(this, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order')); + }) + }, + error: function (err){ + // reset to old name + $(el_item_name).text(truncate_filename(options.name, TRUNCATE_LENGTH)); + $(el_item_name).show(); + + // hide item name editor + $(el_item_name_editor).hide(); + $(el_item_name_editor).val(html_encode($(el_item).attr('data-name'))); + + //show error + if(err.message){ + UIAlert(err.message) + } + }, + }); +} + +/** + * Deletes the given item with path. + * + * @param {string} path - path of the item to delete + * @returns {Promise} + */ +window.delete_item_with_path = async function(path){ + try{ + await puter.fs.delete({ + paths: path, + descendantsOnly: false, + recursive: true, + }); + }catch(err){ + UIAlert(err.responseText); + } +} + +window.undo_last_action = async()=>{ + if (actions_history.length > 0) { + const last_action = actions_history.pop(); + + // Undo the create file action + if (last_action.operation === 'create_file' || last_action.operation === 'create_folder') { + const lastCreatedItem = last_action.data; + undo_create_file_or_folder(lastCreatedItem); + } else if(last_action.operation === 'rename') { + const {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url} = last_action.data; + rename_file(options, old_name, new_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true); + } else if(last_action.operation === 'upload') { + const files = last_action.data; + undo_upload(files); + } else if(last_action.operation === 'copy') { + const files = last_action.data; + undo_copy(files); + } else if(last_action.operation === 'move') { + const items = last_action.data; + undo_move(items); + } else if(last_action.operation === 'delete') { + const items = last_action.data; + undo_delete(items); + } + } +} + +window.undo_create_file_or_folder = async(item)=>{ + await window.delete_item(item); +} + +window.undo_upload = async(files)=>{ + for (const file of files) { + await window.delete_item_with_path(file); + } +} + +window.undo_copy = async(files)=>{ + for (const file of files) { + await window.delete_item_with_path(file); + } +} + +window.undo_move = async(items)=>{ + for (const item of items) { + const el = await get_html_element_from_options(item.options); + console.log(item.original_path) + move_items([el], path.dirname(item.original_path), true); + } +} + +window.undo_delete = async(items)=>{ + for (const item of items) { + const el = await get_html_element_from_options(item.options); + let metadata = $(el).attr('data-metadata') === '' ? {} : JSON.parse($(el).attr('data-metadata')) + move_items([el], path.dirname(metadata.original_path), true); + } +} + + +window.get_html_element_from_options = async function(options){ + const item_id = global_element_id++; + + options.disabled = options.disabled ?? false; + options.visible = options.visible ?? 'visible'; // one of 'visible', 'revealed', 'hidden' + options.is_dir = options.is_dir ?? false; + options.is_selected = options.is_selected ?? false; + options.is_shared = options.is_shared ?? false; + options.is_shortcut = options.is_shortcut ?? 0; + options.is_trash = options.is_trash ?? false; + options.metadata = options.metadata ?? ''; + options.multiselectable = options.multiselectable ?? true; + options.shortcut_to = options.shortcut_to ?? ''; + options.shortcut_to_path = options.shortcut_to_path ?? ''; + options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1); + options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false); + const is_shared_with_me = (options.path !== '/'+window.user.username && !options.path.startsWith('/'+window.user.username+'/')); + + let website_url = determine_website_url(options.path); + + // do a quick check to see if the target parent has any file type restrictions + const appendto_allowed_file_types = $(options.appendTo).attr('data-allowed_file_types') + if(!window.check_fsentry_against_allowed_file_types_string({is_dir: options.is_dir, name:options.name, type:options.type}, appendto_allowed_file_types)) + options.disabled = true; + + // -------------------------------------------------------- + // HTML for Item + // -------------------------------------------------------- + let h = ''; + h += `
`; + + // spinner + h += `
`; + h += `
`; + // modified + h += `
`; + h += `${options.modified === 0 ? '-' : timeago.format(options.modified*1000)}`; + h += `
`; + // size + h += `
`; + h += `${options.size ? byte_format(options.size) : '-'}`; + h += `
`; + // type + h += `
`; + if(options.is_dir) + h += `Folder`; + else + h += `${options.type ? html_encode(options.type) : '-'}`; + h += `
`; + + + // icon + h += `
`; + h += ``; + h += `
`; + // badges + h += `
`; + // website badge + h += ``; + // link badge + h += ``; + + // shared badge + h += ``; + // owner-shared badge + h += ``; + // shortcut badge + h += ``; + + h += `
`; + + // name + h += `${html_encode(truncate_filename(options.name, TRUNCATE_LENGTH)).replaceAll(' ', ' ')}` + // name editor + h += `` + h += `
`; + + return h; +} \ No newline at end of file diff --git a/src/initgui.js b/src/initgui.js index 306e070fc..097dd0374 100644 --- a/src/initgui.js +++ b/src/initgui.js @@ -1677,6 +1677,14 @@ window.initgui = async function(){ } return false; } + //----------------------------------------------------------------------------- + // Undo + // ctrl/command + z, will undo last action + //----------------------------------------------------------------------------- + if((e.ctrlKey || e.metaKey) && e.which === 90){ + undo_last_action(); + return false; + } }); $(document).on('click', '.remove-permission-link', async function(e){