Skip to content

Commit 6ed4bdf

Browse files
committed
feat(drive): paste images/text from the OS clipboard (Ctrl/⌘+V)
Pressing Ctrl/⌘+V in a space with an image (e.g. a screenshot) or text on the OS clipboard now creates the file in the open folder — no need to save it first. Files/images on the clipboard are uploaded as-is (auto-named when the clipboard gives no name); plain text becomes a .txt file. Works in normal, Zero-Knowledge (encrypted client-side) and Public spaces, reusing the existing upload path. The in-app file clipboard (copy/cut of files) still wins on Ctrl+V; the OS paste only runs when there's nothing to move internally, and never while an input/editor is focused or the viewer is open. Shortcuts help + docs updated.
1 parent 992efb5 commit 6ed4bdf

3 files changed

Lines changed: 60 additions & 5 deletions

File tree

‎apps/web/src/app/(app)/drive/page.tsx‎

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ export default function EspacesPage() {
483483
}
484484

485485
const onUpload = useCallback(
486-
async (list: FileList | null) => {
486+
async (list: FileList | File[] | null) => {
487487
if (!list || list.length === 0 || !currentFolderId) return;
488488
const arr = Array.from(list);
489489
setError(null);
@@ -566,6 +566,56 @@ export default function EspacesPage() {
566566
};
567567
}, [activeSpaceId, needPass, onUpload]);
568568

569+
// Paste from the OS clipboard: Ctrl/⌘+V with an image or text on the clipboard drops it straight
570+
// into the open folder. The in-app file clipboard (copy/cut of files) takes precedence and is
571+
// handled on keydown; this only runs when there's nothing to move internally.
572+
const onPaste = useCallback(
573+
async (e: ClipboardEvent) => {
574+
if (!activeSpaceId || !currentFolderId || needPass || busy || viewing || clipboard) return;
575+
const el = document.activeElement as HTMLElement | null;
576+
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable)) return;
577+
const dt = e.clipboardData;
578+
if (!dt) return;
579+
580+
// A readable timestamp for auto-named pastes (no ':' — invalid in file names).
581+
const stamp = () => {
582+
const d = new Date();
583+
const p = (n: number) => String(n).padStart(2, '0');
584+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}h${p(d.getMinutes())}`;
585+
};
586+
587+
// 1) Files / images on the clipboard (screenshots, copied image files).
588+
const files: File[] = [];
589+
for (const item of Array.from(dt.items)) {
590+
if (item.kind !== 'file') continue;
591+
const f = item.getAsFile();
592+
if (!f) continue;
593+
const named = f.name && /\.[a-z0-9]+$/i.test(f.name)
594+
? f
595+
: new File([f], `Image collée ${stamp()}.${(f.type.split('/')[1] || 'png').split('+')[0]}`, { type: f.type || 'image/png' });
596+
files.push(named);
597+
}
598+
if (files.length > 0) {
599+
e.preventDefault();
600+
await onUpload(files);
601+
return;
602+
}
603+
604+
// 2) Plain text → a .txt file.
605+
const text = dt.getData('text/plain');
606+
if (text && text.trim()) {
607+
e.preventDefault();
608+
await onUpload([new File([text], `Texte collé ${stamp()}.txt`, { type: 'text/plain' })]);
609+
}
610+
},
611+
[activeSpaceId, currentFolderId, needPass, busy, viewing, clipboard, onUpload],
612+
);
613+
useEffect(() => {
614+
const h = (e: ClipboardEvent) => void onPaste(e);
615+
window.addEventListener('paste', h);
616+
return () => window.removeEventListener('paste', h);
617+
}, [onPaste]);
618+
569619
function openFile(f: PublicFile) {
570620
setViewing({
571621
name: f.name,
@@ -1226,8 +1276,12 @@ export default function EspacesPage() {
12261276
return;
12271277
}
12281278
if (mod && e.key.toLowerCase() === 'v') {
1229-
e.preventDefault();
1230-
void paste();
1279+
// Internal file clipboard (copy/cut) wins; otherwise let the native `paste` event handle an
1280+
// image/text sitting on the OS clipboard (see the onPaste handler).
1281+
if (clipboard) {
1282+
e.preventDefault();
1283+
void paste();
1284+
}
12311285
return;
12321286
}
12331287
if (mod && e.key.toLowerCase() === 'd') {

‎apps/web/src/app/docs/page.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export default function DocsPage() {
129129
ul: [
130130
L('Bouton « Importer » (sélection multiple).', '“Import” button (multi-select).'),
131131
L('Glisser-déposer depuis le bureau, n’importe où sur la page (un grand overlay apparaît).', 'Drag-and-drop from the desktop anywhere on the page (a full-window overlay appears).'),
132+
L('Coller (Ctrl/⌘ + V) une image ou du texte du presse-papier : le fichier est créé dans le dossier ouvert.', 'Paste (Ctrl/⌘ + V) an image or text from the clipboard: the file is created in the open folder.'),
132133
L('Création d’un fichier vide (texte/markdown) qui s’ouvre directement dans l’éditeur.', 'Create an empty (text/markdown) file that opens straight in the editor.'),
133134
L('Envoi en flux (streaming) : la taille réelle est limitée par votre quota, pas par la RAM.', 'Streaming upload: the real limit is your quota, not RAM.'),
134135
],

‎apps/web/src/lib/i18n.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ const fr: Dict = {
257257
'drive.scTrash': 'Mettre à la corbeille',
258258
'drive.scCopy': 'Copier',
259259
'drive.scCut': 'Couper',
260-
'drive.scPaste': 'Coller',
260+
'drive.scPaste': 'Coller (fichiers, ou image / texte du presse-papier)',
261261
'drive.scDuplicate': 'Dupliquer',
262262
'drive.scClear': 'Retour / annuler la sélection',
263263
'drive.scHelp': 'Afficher cette aide',
@@ -872,7 +872,7 @@ const en: Dict = {
872872
'drive.scTrash': 'Move to trash',
873873
'drive.scCopy': 'Copy',
874874
'drive.scCut': 'Cut',
875-
'drive.scPaste': 'Paste',
875+
'drive.scPaste': 'Paste (files, or a clipboard image / text)',
876876
'drive.scDuplicate': 'Duplicate',
877877
'drive.scClear': 'Back / clear selection',
878878
'drive.scHelp': 'Show this help',

0 commit comments

Comments
 (0)