plugins(files): Feature 5 — Cut/Copy/Paste

- pasteEntry: reads clipboard, copies file with collision resolution,
  handles 'cut' action (trashe source + clear clipboard)
- Paste toolbar button: disabled when clipboard empty, enabled after cut/copy
- Paste in context menu: shown on empty-space right-click when clipboard
  has content
- Folder guard: cut/copy for folders logs not-supported and skips
- updateButtons() toggles paste button state on cut/copy
This commit is contained in:
mirivlad 2026-06-20 19:48:53 +08:00
parent b5d6694afa
commit 281614e1b4
1 changed files with 53 additions and 1 deletions

View File

@ -209,6 +209,7 @@
var openBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'open' }, ['Open']); var openBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'open' }, ['Open']);
var renameBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'rename' }, ['Rename']); var renameBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'rename' }, ['Rename']);
var trashBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'trash' }, ['Trash']); var trashBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'trash' }, ['Trash']);
var pasteBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'paste' }, ['Paste']);
var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' }); var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' });
var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [ var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [
el('option', { value: 'folder-name' }, ['Folders + name']), el('option', { value: 'folder-name' }, ['Folders + name']),
@ -218,7 +219,7 @@
el('option', { value: 'size-desc' }, ['Size']) el('option', { value: 'size-desc' }, ['Size'])
]); ]);
toolbar.appendChild(breadcrumb); toolbar.appendChild(breadcrumb);
[upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); }); [upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, pasteBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); });
containerEl.appendChild(toolbar); containerEl.appendChild(toolbar);
var listContainer = el('div', { className: 'files-list', 'data-files-list': '' }); var listContainer = el('div', { className: 'files-list', 'data-files-list': '' });
@ -252,6 +253,7 @@
openBtn.disabled = !sel; openBtn.disabled = !sel;
renameBtn.disabled = !sel; renameBtn.disabled = !sel;
trashBtn.disabled = !sel; trashBtn.disabled = !sel;
pasteBtn.disabled = !window.__filesClipboard;
} }
function updateBreadcrumb() { function updateBreadcrumb() {
@ -498,6 +500,7 @@
openBtn.addEventListener('click', function () { openEntry(selectedEntry()); }); openBtn.addEventListener('click', function () { openEntry(selectedEntry()); });
renameBtn.addEventListener('click', function () { beginRename(); }); renameBtn.addEventListener('click', function () { beginRename(); });
trashBtn.addEventListener('click', function () { trashEntry(); }); trashBtn.addEventListener('click', function () { trashEntry(); });
pasteBtn.addEventListener('click', function () { pasteEntry(); });
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); }); filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); }); sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
createConfirm.addEventListener('click', confirmCreate); createConfirm.addEventListener('click', confirmCreate);
@ -546,6 +549,10 @@
ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); })); ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }));
ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); })); ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }));
ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); })); ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }));
if (window.__filesClipboard) {
ctxMenu.appendChild(ctxSep());
ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }));
}
} }
ctxMenu.style.display = 'block'; ctxMenu.style.display = 'block';
var mw = ctxMenu.offsetWidth; var mw = ctxMenu.offsetWidth;
@ -595,14 +602,59 @@
function cutEntry(entry) { function cutEntry(entry) {
if (!entry) return; if (!entry) return;
if (entry.type === 'folder') { console.log('[files] Cut for folders not yet supported'); return; }
console.log('[files] Cut:', entry.relativePath); console.log('[files] Cut:', entry.relativePath);
window.__filesClipboard = { action: 'cut', path: entry.relativePath, name: entry.name }; window.__filesClipboard = { action: 'cut', path: entry.relativePath, name: entry.name };
updateButtons();
} }
function copyEntry(entry) { function copyEntry(entry) {
if (!entry) return; if (!entry) return;
if (entry.type === 'folder') { console.log('[files] Copy for folders not yet supported'); return; }
console.log('[files] Copy:', entry.relativePath); console.log('[files] Copy:', entry.relativePath);
window.__filesClipboard = { action: 'copy', path: entry.relativePath, name: entry.name }; window.__filesClipboard = { action: 'copy', path: entry.relativePath, name: entry.name };
updateButtons();
}
function pasteEntry() {
var clip = window.__filesClipboard;
if (!clip || !clip.path) return;
var from = clip.path;
var clipName = clip.name;
var dot = clipName.lastIndexOf('.');
var base = dot > 0 ? clipName.slice(0, dot) : clipName;
var ext = dot > 0 ? clipName.slice(dot) : '';
var maxAttempts = 100;
function tryName(n) {
var newName = n === 1 ? clipName : base + ' (' + n + ')' + ext;
var to = scopedPath(currentPath ? currentPath + '/' + newName : newName);
return api.files.metadata(to).then(function () {
if (n >= maxAttempts) {
console.error('[files] Paste failed: all ' + maxAttempts + ' name variations are taken');
return null;
}
return tryName(n + 1);
}, function () {
return api.files.readText(from).then(function (content) {
return api.files.writeText(to, content, { createIfMissing: true, overwrite: false });
}).then(function () {
if (clip.action === 'cut') {
return api.files.trash(from);
}
});
});
}
tryName(1).then(function (result) {
if (result !== null) {
if (clip.action === 'cut') window.__filesClipboard = null;
loadEntries();
console.log('[files] Pasted:', clip.action, from);
}
}).catch(function (err) {
console.error('[files] Paste failed:', err);
});
} }
var onDocClick = function (e) { var onDocClick = function (e) {