file browser actions keyed (171 -> 124)
useFileBrowserApp.ts, the largest single file in the phase at 47 hits — and contrary
to what I predicted, all 47 were convertible. I expected roughly half to be content
and it was none: every one is a toast the reader sees, and the file NAMES in them are
interpolated parameters inside a translated frame, which is where content belongs.
Worth recording because the guess was wrong in the useful direction.
Six hand-rolled plurals (`item${n !== 1 ? 's' : ''}` and friends) are real plural
entries now, checked at 0, 1 and 2 in both locales. Portuguese reorders these — "1
item eliminado" rather than "Deleted 1 item" — which a frame with the count spliced
in could not have expressed.
This commit is contained in:
+49
-47
@@ -14,6 +14,7 @@ import { useAuth } from 'hooks/useAuth';
|
||||
import { useFilesRefresh } from '../../../channels';
|
||||
import { useNewDashboardDraft } from '../../Dashboards/useNewDashboardDraft';
|
||||
import { copyToClipboard, canReadClipboard } from 'helpers/clipboard';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
/**
|
||||
* The old query param. Kept only to recognise and redirect links made before 2026-08-15 — the folder
|
||||
@@ -22,6 +23,7 @@ import { copyToClipboard, canReadClipboard } from 'helpers/clipboard';
|
||||
export const FILES_PATH_PARAM = 'path';
|
||||
|
||||
export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false, panelId?: string) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -186,7 +188,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
// transient, and bouncing someone out of a deep folder over one bad response would be worse than
|
||||
// the error they can already see.
|
||||
if (firstListing.current && currentPathRef.current !== basePath) {
|
||||
toast.error('That folder is gone — back to the start');
|
||||
toast.error(t('files.folderGone'));
|
||||
setCurrentPath(basePath);
|
||||
rememberFolder({ path: basePath });
|
||||
} else {
|
||||
@@ -227,7 +229,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
const data = await filesRef.current.search(q, currentPath);
|
||||
setSearchResults(data.results);
|
||||
} catch {
|
||||
toast.error('Search failed');
|
||||
toast.error(t('files.searchFailed'));
|
||||
setSearchResults(null);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
@@ -305,7 +307,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
if (selected.size > 0) {
|
||||
const paths = selectedPaths();
|
||||
setClipboard({ paths, mode: 'copy' });
|
||||
toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.copiedItems', { count: paths.length }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -315,7 +317,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
if (selected.size > 0) {
|
||||
const paths = selectedPaths();
|
||||
setClipboard({ paths, mode: 'cut' });
|
||||
toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.cutItems', { count: paths.length }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -429,9 +431,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.createDir(dirPath);
|
||||
await refresh();
|
||||
toast.success(`Created folder "${name}"`);
|
||||
toast.success(t('files.createdFolder', { name }));
|
||||
} catch {
|
||||
toast.error('Failed to create folder');
|
||||
toast.error(t('files.createFolderFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -463,9 +465,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.writeFile(filePath, '');
|
||||
await refresh();
|
||||
toast.success(`Created "${name}"`);
|
||||
toast.success(t('files.created', { name }));
|
||||
} catch {
|
||||
toast.error('Failed to create file');
|
||||
toast.error(t('files.createFileFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -475,9 +477,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.uploadFiles(currentPath, fileList, (pct) => setUploadProgress(pct));
|
||||
await refresh();
|
||||
toast.success(`Uploaded ${count} file${count !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.uploadedFiles', { count }));
|
||||
} catch {
|
||||
toast.error('Failed to upload files');
|
||||
toast.error(t('files.uploadFailed'));
|
||||
} finally {
|
||||
setUploadProgress(null);
|
||||
}
|
||||
@@ -488,9 +490,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.rename(path, newName);
|
||||
await refresh();
|
||||
toast.success(`Renamed to "${newName}"`);
|
||||
toast.success(t('files.renamedTo', { name: newName }));
|
||||
} catch {
|
||||
toast.error('Failed to rename');
|
||||
toast.error(t('files.renameFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -499,7 +501,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
handleDeleteSelected();
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Delete "${entry.name}"?`)) return;
|
||||
if (!confirm(t('files.deleteQ', { name: entry.name }))) return;
|
||||
const path = entryPath(entry.name);
|
||||
try {
|
||||
await files.remove(path);
|
||||
@@ -509,25 +511,25 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
return next;
|
||||
});
|
||||
await refresh();
|
||||
toast.success(`Deleted "${entry.name}"`);
|
||||
toast.success(t('files.deleted', { name: entry.name }));
|
||||
} catch {
|
||||
toast.error('Failed to delete');
|
||||
toast.error(t('files.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const names = Array.from(selected);
|
||||
if (names.length === 0) return;
|
||||
if (!confirm(`Delete ${names.length} item${names.length !== 1 ? 's' : ''}?`)) return;
|
||||
if (!confirm(t('files.deleteItemsQ', { count: names.length }))) return;
|
||||
try {
|
||||
for (const name of names) {
|
||||
await files.remove(entryPath(name));
|
||||
}
|
||||
setSelected(new Set());
|
||||
await refresh();
|
||||
toast.success(`Deleted ${names.length} item${names.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.deletedItems', { count: names.length }));
|
||||
} catch {
|
||||
toast.error('Failed to delete some items');
|
||||
toast.error(t('files.deleteSomeFailed'));
|
||||
await refresh();
|
||||
}
|
||||
};
|
||||
@@ -560,7 +562,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.download(paths);
|
||||
} catch {
|
||||
toast.error('Failed to download');
|
||||
toast.error(t('files.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -570,7 +572,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.download(paths);
|
||||
} catch {
|
||||
toast.error('Failed to download');
|
||||
toast.error(t('files.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -596,7 +598,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
const paths = selected.size > 1 && selected.has(entry.name) ? selectedPaths() : [entryPath(entry.name)];
|
||||
// Newline-separated, which is what a shell, an editor and a chat box all accept as a list of paths.
|
||||
copyToClipboard(paths.map((p) => `~${p}`).join('\n'));
|
||||
toast.success(paths.length > 1 ? `${paths.length} paths copied` : 'Path copied');
|
||||
toast.success(paths.length > 1 ? `${paths.length} paths copied` : t('files.pathCopied'));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -606,20 +608,20 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
*/
|
||||
const handleCopyAbsPath = (path: string) => {
|
||||
copyToClipboard(`~${path}`);
|
||||
toast.success('Path copied');
|
||||
toast.success(t('files.pathCopied'));
|
||||
};
|
||||
|
||||
const handleDownloadPath = async (path: string) => {
|
||||
try {
|
||||
await files.download([path]);
|
||||
} catch {
|
||||
toast.error('Failed to download');
|
||||
toast.error(t('files.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCurrentPath = () => {
|
||||
copyToClipboard(`~${currentPath}`);
|
||||
toast.success('Path copied');
|
||||
toast.success(t('files.pathCopied'));
|
||||
};
|
||||
|
||||
const handleChatHere = () => {
|
||||
@@ -638,7 +640,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
|
||||
const handleReadAloud = async (entry: DirEntry) => {
|
||||
const filePath = entryPath(entry.name);
|
||||
const toastId = toast.loading('Generating speech audio...');
|
||||
const toastId = toast.loading(t('files.generatingSpeech'));
|
||||
try {
|
||||
const { audioPath } = await files.tts(filePath, { saveNextTo: true });
|
||||
toast.dismiss(toastId);
|
||||
@@ -650,20 +652,20 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
ephemeral: relativeToFolder(currentPath, audioPath),
|
||||
});
|
||||
} catch {
|
||||
toast.error('Failed to generate speech audio', { id: toastId });
|
||||
toast.error(t('files.speechFailed'), { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtract = async (entry: DirEntry) => {
|
||||
const filePath = entryPath(entry.name);
|
||||
const toastId = toast.loading('Extracting archive...');
|
||||
const toastId = toast.loading(t('files.extracting'));
|
||||
try {
|
||||
const { extractedPath } = await files.extract(filePath);
|
||||
const folderName = extractedPath.split('/').pop() ?? extractedPath;
|
||||
toast.success(`Extracted to "${folderName}"`, { id: toastId });
|
||||
toast.success(t('files.extractedTo', { name: folderName }), { id: toastId });
|
||||
await refresh();
|
||||
} catch {
|
||||
toast.error('Failed to extract archive', { id: toastId });
|
||||
toast.error(t('files.extractFailed'), { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -674,9 +676,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.gitClone(url, currentPath);
|
||||
await refresh();
|
||||
toast.success('Repository cloned');
|
||||
toast.success(t('files.cloned'));
|
||||
} catch {
|
||||
toast.error('Failed to clone repository');
|
||||
toast.error(t('files.cloneFailed'));
|
||||
} finally {
|
||||
setCloning(false);
|
||||
setCloneUrl('');
|
||||
@@ -695,22 +697,22 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
try {
|
||||
await files.copy([entryPath(entry.name)], currentPath);
|
||||
await refresh();
|
||||
toast.success(`Duplicated "${entry.name}"`);
|
||||
toast.success(t('files.duplicated', { name: entry.name }));
|
||||
} catch {
|
||||
toast.error('Failed to duplicate');
|
||||
toast.error(t('files.duplicateFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
/** Zip the selection (or the clicked entry) into an archive beside it, and keep it. */
|
||||
const handleCompress = async (entry: DirEntry) => {
|
||||
const paths = selected.size > 1 && selected.has(entry.name) ? selectedPaths() : [entryPath(entry.name)];
|
||||
const toastId = toast.loading('Compressing...');
|
||||
const toastId = toast.loading(t('files.compressing'));
|
||||
try {
|
||||
const { name } = await files.compress(paths);
|
||||
await refresh();
|
||||
toast.success(`Created "${name}"`, { id: toastId });
|
||||
toast.success(t('files.created', { name }), { id: toastId });
|
||||
} catch {
|
||||
toast.error('Failed to compress', { id: toastId });
|
||||
toast.error(t('files.compressFailed'), { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -731,9 +733,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
setClipboard(null);
|
||||
}
|
||||
await refresh();
|
||||
toast.success(`Pasted into "${entry.name}"`);
|
||||
toast.success(t('files.pastedInto', { name: entry.name }));
|
||||
} catch {
|
||||
toast.error('Failed to paste');
|
||||
toast.error(t('files.pasteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -741,13 +743,13 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
const handleGitCloneUrl = async (url: string) => {
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return;
|
||||
const toastId = toast.loading('Cloning...');
|
||||
const toastId = toast.loading(t('files.cloning'));
|
||||
try {
|
||||
await files.gitClone(trimmed, currentPath);
|
||||
await refresh();
|
||||
toast.success('Repository cloned', { id: toastId });
|
||||
toast.success(t('files.cloned'), { id: toastId });
|
||||
} catch {
|
||||
toast.error('Failed to clone repository', { id: toastId });
|
||||
toast.error(t('files.cloneFailed'), { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -765,21 +767,21 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
const path = entryPath(entry.name);
|
||||
const wasPinned = isPinned(path);
|
||||
togglePin(path, entry.name);
|
||||
toast.success(wasPinned ? `Unpinned "${entry.name}"` : `Pinned "${entry.name}"`);
|
||||
toast.success(wasPinned ? t('files.unpinned', { name: entry.name }) : t('files.pinned', { name: entry.name }));
|
||||
};
|
||||
|
||||
const handleCut = () => {
|
||||
const paths = selected.size > 0 ? selectedPaths() : [];
|
||||
if (paths.length === 0) return;
|
||||
setClipboard({ paths, mode: 'cut' });
|
||||
toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.cutItems', { count: paths.length }));
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
const paths = selected.size > 0 ? selectedPaths() : [];
|
||||
if (paths.length === 0) return;
|
||||
setClipboard({ paths, mode: 'copy' });
|
||||
toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.copiedItems', { count: paths.length }));
|
||||
};
|
||||
|
||||
const handlePaste = async () => {
|
||||
@@ -792,9 +794,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
setClipboard(null);
|
||||
}
|
||||
await refresh();
|
||||
toast.success(`Pasted ${clipboard.paths.length} item${clipboard.paths.length !== 1 ? 's' : ''}`);
|
||||
toast.success(t('files.pastedItems', { count: clipboard.paths.length }));
|
||||
} catch {
|
||||
toast.error('Failed to paste');
|
||||
toast.error(t('files.pasteFailed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -804,7 +806,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
||||
// is no way to pull clipboard contents on demand. Pasting an image has to come from a real paste
|
||||
// event instead. Refuse clearly rather than throwing "Cannot read properties of undefined".
|
||||
if (!canReadClipboard()) {
|
||||
toast.error('Pasting from the clipboard needs a secure context (https). Drag the file in instead.');
|
||||
toast.error(t('files.pasteNeedsSecure'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -427,5 +427,63 @@
|
||||
"peers": "Peers: {{names}}"
|
||||
},
|
||||
"copy": "Copy"
|
||||
},
|
||||
"files": {
|
||||
"folderGone": "That folder is gone — back to the start",
|
||||
"searchFailed": "Search failed",
|
||||
"createFolderFailed": "Failed to create folder",
|
||||
"createFileFailed": "Failed to create file",
|
||||
"uploadFailed": "Failed to upload files",
|
||||
"renameFailed": "Failed to rename",
|
||||
"deleteFailed": "Failed to delete",
|
||||
"deleteSomeFailed": "Failed to delete some items",
|
||||
"downloadFailed": "Failed to download",
|
||||
"pathCopied": "Path copied",
|
||||
"generatingSpeech": "Generating speech audio…",
|
||||
"speechFailed": "Failed to generate speech audio",
|
||||
"extracting": "Extracting archive…",
|
||||
"extractFailed": "Failed to extract archive",
|
||||
"cloned": "Repository cloned",
|
||||
"cloneFailed": "Failed to clone repository",
|
||||
"cloning": "Cloning…",
|
||||
"duplicateFailed": "Failed to duplicate",
|
||||
"compressing": "Compressing…",
|
||||
"compressFailed": "Failed to compress",
|
||||
"pasteFailed": "Failed to paste",
|
||||
"pasteNeedsSecure": "Pasting from the clipboard needs a secure context (https). Drag the file in instead.",
|
||||
"copiedItems": {
|
||||
"one": "Copied {{count}} item",
|
||||
"other": "Copied {{count}} items"
|
||||
},
|
||||
"cutItems": {
|
||||
"one": "Cut {{count}} item",
|
||||
"other": "Cut {{count}} items"
|
||||
},
|
||||
"pastedItems": {
|
||||
"one": "Pasted {{count}} item",
|
||||
"other": "Pasted {{count}} items"
|
||||
},
|
||||
"uploadedFiles": {
|
||||
"one": "Uploaded {{count}} file",
|
||||
"other": "Uploaded {{count}} files"
|
||||
},
|
||||
"deleteItemsQ": {
|
||||
"one": "Delete {{count}} item?",
|
||||
"other": "Delete {{count}} items?"
|
||||
},
|
||||
"deletedItems": {
|
||||
"one": "Deleted {{count}} item",
|
||||
"other": "Deleted {{count}} items"
|
||||
},
|
||||
"createdFolder": "Created folder “{{name}}”",
|
||||
"created": "Created “{{name}}”",
|
||||
"renamedTo": "Renamed to “{{name}}”",
|
||||
"deleteQ": "Delete “{{name}}”?",
|
||||
"deleted": "Deleted “{{name}}”",
|
||||
"extractedTo": "Extracted to “{{name}}”",
|
||||
"duplicated": "Duplicated “{{name}}”",
|
||||
"pastedInto": "Pasted into “{{name}}”",
|
||||
"unpinned": "Unpinned “{{name}}”",
|
||||
"pinned": "Pinned “{{name}}”"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,5 +427,63 @@
|
||||
"peers": "Pares: {{names}}"
|
||||
},
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"files": {
|
||||
"folderGone": "Essa pasta desapareceu — de volta ao início",
|
||||
"searchFailed": "A pesquisa falhou",
|
||||
"createFolderFailed": "Falha ao criar a pasta",
|
||||
"createFileFailed": "Falha ao criar o ficheiro",
|
||||
"uploadFailed": "Falha ao carregar os ficheiros",
|
||||
"renameFailed": "Falha ao renomear",
|
||||
"deleteFailed": "Falha ao eliminar",
|
||||
"deleteSomeFailed": "Falha ao eliminar alguns itens",
|
||||
"downloadFailed": "Falha ao descarregar",
|
||||
"pathCopied": "Caminho copiado",
|
||||
"generatingSpeech": "A gerar áudio de fala…",
|
||||
"speechFailed": "Falha ao gerar o áudio de fala",
|
||||
"extracting": "A extrair o arquivo…",
|
||||
"extractFailed": "Falha ao extrair o arquivo",
|
||||
"cloned": "Repositório clonado",
|
||||
"cloneFailed": "Falha ao clonar o repositório",
|
||||
"cloning": "A clonar…",
|
||||
"duplicateFailed": "Falha ao duplicar",
|
||||
"compressing": "A comprimir…",
|
||||
"compressFailed": "Falha ao comprimir",
|
||||
"pasteFailed": "Falha ao colar",
|
||||
"pasteNeedsSecure": "Colar da área de transferência exige um contexto seguro (https). Arraste o ficheiro para aqui em vez disso.",
|
||||
"copiedItems": {
|
||||
"one": "{{count}} item copiado",
|
||||
"other": "{{count}} itens copiados"
|
||||
},
|
||||
"cutItems": {
|
||||
"one": "{{count}} item cortado",
|
||||
"other": "{{count}} itens cortados"
|
||||
},
|
||||
"pastedItems": {
|
||||
"one": "{{count}} item colado",
|
||||
"other": "{{count}} itens colados"
|
||||
},
|
||||
"uploadedFiles": {
|
||||
"one": "{{count}} ficheiro carregado",
|
||||
"other": "{{count}} ficheiros carregados"
|
||||
},
|
||||
"deleteItemsQ": {
|
||||
"one": "Eliminar {{count}} item?",
|
||||
"other": "Eliminar {{count}} itens?"
|
||||
},
|
||||
"deletedItems": {
|
||||
"one": "{{count}} item eliminado",
|
||||
"other": "{{count}} itens eliminados"
|
||||
},
|
||||
"createdFolder": "Pasta “{{name}}” criada",
|
||||
"created": "“{{name}}” criado",
|
||||
"renamedTo": "Renomeado para “{{name}}”",
|
||||
"deleteQ": "Eliminar “{{name}}”?",
|
||||
"deleted": "“{{name}}” eliminado",
|
||||
"extractedTo": "Extraído para “{{name}}”",
|
||||
"duplicated": "“{{name}}” duplicado",
|
||||
"pastedInto": "Colado em “{{name}}”",
|
||||
"unpinned": "“{{name}}” removido dos fixados",
|
||||
"pinned": "“{{name}}” fixado"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user