diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js index 3c2f6e294f..1b9f5a1873 100644 --- a/src/gui/src/UI/UIItem.js +++ b/src/gui/src/UI/UIItem.js @@ -29,6 +29,7 @@ import path from "../lib/path.js" import truncate_filename from '../helpers/truncate_filename.js'; import launch_app from "../helpers/launch_app.js" import open_item from "../helpers/open_item.js" +import item_icon from '../helpers/item_icon.js'; function UIItem(options){ const matching_appendto_count = $(options.appendTo).length; @@ -1248,6 +1249,70 @@ function UIItem(options){ }); } // ------------------------------------------- + // Duplicate + // ------------------------------------------- + if(!is_trashed && !is_trash && !options.is_dir){ + menu_items.push({ + html: i18n('duplicate'), + onClick: async function(){ + const source_path = options.path; + const dest_dir = path.dirname(source_path); + const item_container = $(el_item).closest('.item-container'); + + try { + // Copy the file to the same directory with auto-renaming + const resp = await puter.fs.copy({ + source: source_path, + destination: dest_dir, + dedupeName: true, + overwrite: false, + }); + + // Get the copied item information + const copied_item = resp[0].copied; + + // Wait a brief moment to allow socket event to potentially add the item + await new Promise(resolve => setTimeout(resolve, 100)); + + // Check if item already exists in the UI (socket event may have added it) + const existing_item = $(`.item[data-uid='${copied_item.uid}']`); + if(existing_item.length === 0){ + // Item doesn't exist yet, manually add it (socket event skipped it) + const icon = await item_icon(copied_item); + + UIItem({ + appendTo: item_container, + uid: copied_item.uid, + immutable: copied_item.immutable || (copied_item.writable === false ? 1 : 0), + associated_app_name: copied_item.associated_app?.name || null, + path: copied_item.path, + icon: icon, + name: copied_item.name, + size: copied_item.size || 0, + type: copied_item.type || null, + modified: copied_item.modified || 0, + is_dir: copied_item.is_dir || false, + is_shared: copied_item.is_shared || false, + is_shortcut: copied_item.is_shortcut || 0, + shortcut_to: copied_item.shortcut_to || '', + shortcut_to_path: copied_item.shortcut_to_path || '', + suggested_apps: copied_item.suggested_apps || [], + }); + } + + // Sort the container to show the new item in the correct position + window.sort_items(item_container, $(item_container).attr('data-sort_by'), $(item_container).attr('data-sort_order')); + } catch(err) { + if(err.message){ + UIAlert(err.message); + } else { + UIAlert(i18n('something_went_wrong')); + } + } + } + }); + } + // ------------------------------------------- // Paste Into Folder // ------------------------------------------- if($(el_item).attr('data-is_dir') === '1' && !is_trashed && !is_trash){ diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 99bce7db0c..11d2ae9807 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -3326,9 +3326,19 @@ window.scale_window = (el_window)=>{ } window.update_explorer_footer_item_count = function(el_window){ - //update dir count in explorer footer - let item_count = $(el_window).find('.item').length; - $(el_window).find('.explorer-footer .explorer-footer-item-count').html(item_count + ` ${i18n('item')}` + (item_count == 0 || item_count > 1 ? `${i18n('plural_suffix')}` : '')); + const $window = $(el_window); + const items = $window.find('.item'); + const item_count = items.length; + let footer_label = item_count + ` ${i18n('item')}` + (item_count === 0 || item_count > 1 ? `${i18n('plural_suffix')}` : ''); + + if(window.user_preferences?.show_hidden_files){ + const hidden_item_count = items.filter((_, item) => item.dataset?.name?.startsWith('.')).length; + if(hidden_item_count > 0){ + footer_label += ` (${hidden_item_count} ${i18n('hidden')})`; + } + } + + $window.find('.explorer-footer .explorer-footer-item-count').html(footer_label); } window.update_explorer_footer_selected_items_count = function(el_window){ diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index f855fa62e6..1eee24c297 100644 --- a/src/gui/src/helpers.js +++ b/src/gui/src/helpers.js @@ -774,10 +774,18 @@ window.show_or_hide_files = (item_containers) => { const show_hidden_files = window.user_preferences.show_hidden_files; const class_to_add = show_hidden_files ? 'item-revealed' : 'item-hidden'; const class_to_remove = show_hidden_files ? 'item-hidden' : 'item-revealed'; - $(item_containers) + const $containers = $(item_containers); + $containers .find('.item') .filter((_, item) => item.dataset.name.startsWith('.')) .removeClass(class_to_remove).addClass(class_to_add); + + $containers.each(function(){ + const $window = $(this).closest('.window'); + if($window.length){ + window.update_explorer_footer_item_count($window); + } + }); } window.create_folder = async(basedir, appendto_element)=>{ diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 613120afc0..c60993660a 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -81,6 +81,7 @@ const en = { copying: "Copying", copying_file: "Copying %%", cover: 'Cover', + duplicate: 'Duplicate', create_account: "Create Account", create_free_account: "Create Free Account", create_shortcut: "Create Shortcut", @@ -274,6 +275,7 @@ const en = { shortcut_to: "Shortcut to", show_all_windows: "Show All Windows", show_hidden: 'Show hidden', + hidden: 'hidden', sign_in_with_puter: "Sign in with Puter", sign_up: "Sign Up", signing_in: "Signing in…", diff --git a/src/parsers/strataparse/strata_impls/ContextSwitchingPStratumImpl.js b/src/parsers/strataparse/strata_impls/ContextSwitchingPStratumImpl.js index cb351104d4..91f6bc5bb6 100644 --- a/src/parsers/strataparse/strata_impls/ContextSwitchingPStratumImpl.js +++ b/src/parsers/strataparse/strata_impls/ContextSwitchingPStratumImpl.js @@ -95,4 +95,31 @@ export default class ContextSwitchingPStratumImpl { return { done: true, value: 'ran out of parsers' }; } + + duplicateContext(api) { + const currentContextName = this.stack_top.context_name; + const newContextName = this.generateUniqueContextName(currentContextName); + + this.contexts[newContextName] = this.contexts[currentContextName].map(parser => { + if (parser.hasOwnProperty('transition')) { + return { + ...parser, + parser: AcceptParserUtil.adapt(parser.parser), + }; + } + return AcceptParserUtil.adapt(parser); + }); + + return { done: false, value: `Context duplicated as ${newContextName}` }; + } + + generateUniqueContextName(baseName) { + let counter = 1; + let newName = `${baseName}_copy`; + while (this.contexts.hasOwnProperty(newName)) { + newName = `${baseName}_copy_${counter}`; + counter++; + } + return newName; + } }