mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: keep deleted files in a Files trash with Restore, Delete permanently and Empty trash
This commit is contained in:
@@ -376,4 +376,28 @@ describe('FileBrowser', () => {
|
||||
expect(screen.queryByRole('menu')).toBeNull();
|
||||
expect(onClearSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("offers Restore and Delete permanently in the trash, and Empty trash in its toolbar", () => {
|
||||
const onRestore = vi.fn(async () => {});
|
||||
const onEmptyTrash = vi.fn();
|
||||
const trashed = { ...resource(OWN[1]), trash: { deletedAt: '2026-10-20T10:00:00Z', fromParentId: null, batchId: 'b1' } };
|
||||
act(() => { useFileStore.setState({ trashCount: 1 }); });
|
||||
renderBrowser({ resources: [trashed], props: { currentPath: '/.Trash', onRestore, onEmptyTrash } });
|
||||
act(() => { useFileStore.setState({ currentParentId: '*trash', trashCount: 1 }); });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'empty_trash' }));
|
||||
expect(onEmptyTrash).toHaveBeenCalled();
|
||||
|
||||
fireEvent.contextMenu(within(list()).getByText('notes.txt'));
|
||||
expect(screen.queryByText('rename')).toBeNull();
|
||||
fireEvent.click(screen.getByText('restore'));
|
||||
expect(onRestore).toHaveBeenCalledWith(['notes.txt']);
|
||||
expect(screen.getByRole('columnheader', { name: 'deleted' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("lists the trash in the sidebar", () => {
|
||||
const { onNavigate } = renderBrowser();
|
||||
fireEvent.click(within(tree()).getByRole('button', { name: /trash/ }));
|
||||
expect(onNavigate).toHaveBeenCalledWith('/.Trash', '*trash');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Upload, FolderPlus, Download, Trash2, Pencil, Copy, Clipboard, Scissors, Info,
|
||||
FilePlus, CopyPlus, AlertCircle, FolderUp, Share2, SquarePen, Eye, FolderOpen,
|
||||
RefreshCw, Star, StarOff, Plus, CheckSquare, File,
|
||||
RefreshCw, Star, StarOff, Plus, CheckSquare, File, RotateCcw,
|
||||
} from "@/components/icons";
|
||||
import { useIsDesktop, useIsMobile, useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||
@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
|
||||
import { PromptDialog } from "@/components/ui/prompt-dialog";
|
||||
import { fileNameProblem } from "@/lib/file-name-rules";
|
||||
import { EmptyFolder, NoSearchResults } from "@/components/files/file-empty-state";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { isAudioFile, isImageFile, isPdfFile, isTextFile, isVideoFile } from "@/components/files/file-icons";
|
||||
import { loadFilesSettings, saveFilesSettings, type FilesSettings } from "@/components/files/files-settings";
|
||||
import { FilesSidebar, type CreateKind } from "@/components/files/files-sidebar";
|
||||
@@ -26,6 +27,7 @@ import { INTERNAL_DRAG_TYPE, anchorEvent, isInternalDrag, readDraggedNames } fro
|
||||
import { ResizeHandle } from "@/components/layout/resize-handle";
|
||||
import { getDroppedFilesAndFolders } from "@/lib/webdav/drop-utils";
|
||||
import { resourceToView, useFileStore, isSharedFilesPage, sharedFilePages, type FileResource } from "@/stores/file-store";
|
||||
import { TRASH_LOCATION, TRASH_PATH, isTrashLocation } from "@/lib/files-trash";
|
||||
import { PluginActions, usePluginActions } from "@/components/plugins/plugin-actions";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { ShareCollectionDialog } from "@/components/settings/share-collection-dialog";
|
||||
@@ -60,8 +62,13 @@ interface FileBrowserProps {
|
||||
onUploadFiles: (files: File[]) => Promise<void>;
|
||||
onUploadFolder: (files: File[]) => Promise<void>;
|
||||
onCancelUpload: () => void;
|
||||
/** Delete: into the trash, or for good when in the trash or a shared drive. */
|
||||
onDelete: (name: string) => Promise<void>;
|
||||
onBatchDelete: (names: string[]) => Promise<void>;
|
||||
/** In the trash: put items back where they came from. */
|
||||
onRestore?: (names: string[]) => Promise<void>;
|
||||
/** In the trash: delete everything in it for good. */
|
||||
onEmptyTrash?: () => void;
|
||||
onRename: (oldName: string, newName: string) => Promise<void>;
|
||||
onDownload: (name: string) => Promise<void>;
|
||||
onBatchDownload: (names: string[]) => Promise<void>;
|
||||
@@ -118,6 +125,16 @@ type MenuData = { kind: "item"; resource: FileResource } | { kind: "empty" } | {
|
||||
|
||||
const SIDEBAR_DEFAULT_WIDTH = 256;
|
||||
|
||||
/** A folder's path ("/Documents/Taxes") from the folder list. */
|
||||
function pathOfFolder(folders: FileResource[], folder: FileResource): string {
|
||||
const names = [folder.name];
|
||||
const byId = new Map(folders.map((f) => [f.id, f]));
|
||||
for (let cur = folder.parentId ? byId.get(folder.parentId) : undefined; cur && names.length < 64; cur = cur.parentId ? byId.get(cur.parentId) : undefined) {
|
||||
names.unshift(cur.name);
|
||||
}
|
||||
return "/" + names.join("/");
|
||||
}
|
||||
|
||||
function isTypingTarget(target: EventTarget | null): boolean {
|
||||
const el = target as HTMLElement | null;
|
||||
if (!el) return false;
|
||||
@@ -139,6 +156,8 @@ export function FileBrowser({
|
||||
onCancelUpload,
|
||||
onDelete,
|
||||
onBatchDelete,
|
||||
onRestore,
|
||||
onEmptyTrash,
|
||||
onRename,
|
||||
onDownload,
|
||||
onBatchDownload,
|
||||
@@ -196,6 +215,10 @@ export function FileBrowser({
|
||||
const hasListed = useFileStore((s) => s.hasListed);
|
||||
const currentParentId = useFileStore((s) => s.currentParentId);
|
||||
const pathStack = useFileStore((s) => s.pathStack);
|
||||
const trashCount = useFileStore((s) => s.trashCount);
|
||||
// The trash lists deleted items: they can be restored, previewed or
|
||||
// deleted for good, nothing else.
|
||||
const inTrash = isTrashLocation(currentParentId);
|
||||
useEffect(() => {
|
||||
if (storeClient) void loadSharedRoots();
|
||||
}, [storeClient, loadSharedRoots]);
|
||||
@@ -346,12 +369,15 @@ export function FileBrowser({
|
||||
// Whether the user may add items to the folder on screen. Nothing can be
|
||||
// put on a page listing files shared on their own.
|
||||
const currentFolder = currentParentId ? allFolders.find((f) => f.id === currentParentId) : null;
|
||||
const canCreate = !accountPickerMode && !isSharedFilesPage(currentParentId)
|
||||
const canCreate = !accountPickerMode && !isSharedFilesPage(currentParentId) && !inTrash
|
||||
&& (currentFolder?.myRights?.mayAddChildren ?? true);
|
||||
|
||||
// ---- Listing ---------------------------------------------------------------
|
||||
const displayResources = useMemo(() => {
|
||||
let list = resources;
|
||||
// In the trash the date column is when an item was deleted.
|
||||
let list = inTrash
|
||||
? resources.map((r) => (r.trash?.deletedAt ? { ...r, lastModified: r.trash.deletedAt } : r))
|
||||
: resources;
|
||||
if (!settings.showHiddenFiles) list = list.filter((r) => !r.name.startsWith("."));
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
@@ -375,7 +401,7 @@ export function FileBrowser({
|
||||
if (cmp === 0) cmp = a.name.localeCompare(b.name);
|
||||
return sortDir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
}, [resources, settings.showHiddenFiles, searchQuery, sortKey, sortDir]);
|
||||
}, [resources, inTrash, settings.showHiddenFiles, searchQuery, sortKey, sortDir]);
|
||||
|
||||
// Folders other principals shared with the user, listed at the root next to
|
||||
// the user's own (#1181).
|
||||
@@ -407,11 +433,12 @@ export function FileBrowser({
|
||||
// Inside a folder someone shared with the user, the trail starts there.
|
||||
const insideShared = !!pathStack[1]?.id?.includes(":");
|
||||
const list: Crumb[] = [{ id: null, name: insideShared ? t("shared_with_me") : t("my_files"), path: "/" }];
|
||||
if (inTrash) return [...list, { id: TRASH_LOCATION, name: t("trash"), path: TRASH_PATH }];
|
||||
segments.forEach((name, i) => {
|
||||
list.push({ id: pathStack[i + 1]?.id ?? null, name, path: "/" + segments.slice(0, i + 1).join("/") });
|
||||
});
|
||||
return list;
|
||||
}, [currentPath, pathStack, t]);
|
||||
}, [currentPath, pathStack, inTrash, t]);
|
||||
const folderName = crumbs[crumbs.length - 1].name;
|
||||
|
||||
const navigateCrumb = useCallback((crumb: Crumb) => {
|
||||
@@ -438,6 +465,8 @@ export function FileBrowser({
|
||||
const openResource = useCallback((resource: FileResource) => {
|
||||
// A symlink has nothing to open (Stalwart 1.0).
|
||||
if (resource.isSymlink) return;
|
||||
// A deleted folder is restored before it is opened; a deleted file can be looked at.
|
||||
if (resource.trash && (resource.isDirectory || !isPreviewable(resource.name))) return;
|
||||
if (resource.isDirectory) {
|
||||
onNavigate(pathOf(resource), resource.id);
|
||||
} else if (isOfficeEditable?.(resource.name) && onEditFile) {
|
||||
@@ -456,6 +485,15 @@ export function FileBrowser({
|
||||
else onPreviewFile(resource.name);
|
||||
}, [onPreviewImage, onPreviewFile]);
|
||||
|
||||
// Where a deleted item came from, as its row's tooltip in the trash.
|
||||
const trashNote = useCallback((r: FileResource): string | undefined => {
|
||||
if (!r.trash) return undefined;
|
||||
const from = r.trash.fromParentId;
|
||||
const folder = from ? allFolders.find((f) => f.id === from) : null;
|
||||
const path = from === null ? t("my_files") : folder ? pathOfFolder(allFolders, folder) : null;
|
||||
return path ? t("deleted_from", { folder: path }) : undefined;
|
||||
}, [allFolders, t]);
|
||||
|
||||
// ---- Selection -------------------------------------------------------------
|
||||
const selectRange = useCallback((from: string, to: string, additive: boolean) => {
|
||||
const names = displayResources.map((r) => r.name);
|
||||
@@ -514,7 +552,7 @@ export function FileBrowser({
|
||||
}, [onMoveTo]);
|
||||
|
||||
const itemDragProps = useCallback((resource: FileResource) => ({
|
||||
draggable: !touch,
|
||||
draggable: !touch && !inTrash,
|
||||
onDragStart: (e: React.DragEvent) => {
|
||||
const names = selectedResources.has(resource.name) ? [...selectedResources] : [resource.name];
|
||||
e.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(names));
|
||||
@@ -536,7 +574,7 @@ export function FileBrowser({
|
||||
const names = readDraggedNames(e).filter((n) => n !== resource.name);
|
||||
if (names.length > 0) moveNames(names, resource.id);
|
||||
},
|
||||
}), [touch, selectedResources, moveNames]);
|
||||
}), [touch, inTrash, selectedResources, moveNames]);
|
||||
|
||||
const runUpload = useCallback(async (files: File[], asFolder: boolean) => {
|
||||
if (files.length === 0) return;
|
||||
@@ -683,6 +721,7 @@ export function FileBrowser({
|
||||
selectAllVisible();
|
||||
return;
|
||||
}
|
||||
if (inTrash && (mod || e.key === "F2") && ["c", "x", "v", "f2"].includes(e.key.toLowerCase())) return;
|
||||
if (mod && e.key.toLowerCase() === "c" && selected.length > 0) {
|
||||
if (window.getSelection()?.toString()) return;
|
||||
e.preventDefault();
|
||||
@@ -750,7 +789,7 @@ export function FileBrowser({
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [
|
||||
showNewFolder, showNewTextFile, renameTarget, renameFolder, shareTargetId, accountPickerMode, selectedList, singleSelected,
|
||||
showNewFolder, showNewTextFile, renameTarget, renameFolder, shareTargetId, accountPickerMode, selectedList, singleSelected, inTrash,
|
||||
selectAllVisible, onCopy, onCut, onPaste, clipboard, searchQuery, onClearSelection, deleteNames, openResource,
|
||||
focusedName, onToggleSelect, currentPath, navigateUp, viewMode, gridColumns, moveFocus,
|
||||
]);
|
||||
@@ -779,6 +818,25 @@ export function FileBrowser({
|
||||
: menuData?.kind === "item" ? [menuData.resource] : [];
|
||||
|
||||
const renderItemMenu = (resource: FileResource) => {
|
||||
if (inTrash) {
|
||||
const names = menuSelection.map((r) => r.name);
|
||||
const one = menuSelection.length === 1 ? menuSelection[0] : null;
|
||||
return (
|
||||
<>
|
||||
{onRestore && (
|
||||
<ContextMenuItem icon={RotateCcw} label={t("restore")} onClick={() => { closeMenu(); void onRestore(names); }} />
|
||||
)}
|
||||
{one && !one.isDirectory && !one.isSymlink && isPreviewable(one.name) && (
|
||||
<ContextMenuItem icon={Eye} label={t("preview")} onClick={() => { closeMenu(); previewResource(one); }} />
|
||||
)}
|
||||
{one && !one.isDirectory && !one.isSymlink && (
|
||||
<ContextMenuItem icon={Download} label={t("download")} onClick={() => { closeMenu(); void onDownload(one.name); }} />
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem icon={Trash2} label={t("delete_forever")} destructive onClick={() => { closeMenu(); deleteNames(names); }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (menuSelection.length > 1) {
|
||||
const names = menuSelection.map((r) => r.name);
|
||||
const fileCount = menuSelection.filter((r) => !r.isDirectory && !r.isSymlink).length;
|
||||
@@ -910,6 +968,9 @@ export function FileBrowser({
|
||||
onRevealRecent={fromDrawer((id: string) => void onRevealNode(id))}
|
||||
onDownloadRecent={fromDrawer((id: string) => void onDownloadNode(id))}
|
||||
onRemoveRecent={onRemoveRecent}
|
||||
trashCount={trashCount}
|
||||
onOpenTrash={fromDrawer(() => onNavigate(TRASH_PATH, TRASH_LOCATION))}
|
||||
onDropToTrash={(names) => deleteNames(names)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -948,6 +1009,9 @@ export function FileBrowser({
|
||||
// Until the first listing is in, an empty list means "not loaded yet".
|
||||
if ((isLoading || !hasListed) && resources.length === 0) return <ListSkeleton />;
|
||||
if (showEmpty && searchQuery) return <NoSearchResults query={searchQuery} onClear={() => setSearchQuery("")} />;
|
||||
if (showEmpty && inTrash) {
|
||||
return <EmptyState icon={Trash2} title={t("trash_empty_title")} description={t("trash_empty_hint")} />;
|
||||
}
|
||||
if (showEmpty) {
|
||||
return (
|
||||
<EmptyFolder
|
||||
@@ -970,6 +1034,7 @@ export function FileBrowser({
|
||||
iconOptions,
|
||||
getImageUrl,
|
||||
handlers,
|
||||
itemNote: inTrash ? trashNote : undefined,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
@@ -984,6 +1049,7 @@ export function FileBrowser({
|
||||
allSelected={allSelected}
|
||||
someSelected={someSelected}
|
||||
onToggleAll={() => (allSelected ? onClearSelection() : selectAllVisible())}
|
||||
dateLabel={inTrash ? t("deleted") : undefined}
|
||||
/>
|
||||
))}
|
||||
{(sharedAtRoot.length > 0 || sharedPagesAtRoot.length > 0) && (
|
||||
@@ -1025,7 +1091,24 @@ export function FileBrowser({
|
||||
/>
|
||||
);
|
||||
|
||||
const selectionActions = selectedList.length > 0 ? {
|
||||
const selectionActions = selectedList.length > 0 && inTrash ? {
|
||||
count: selectedList.length,
|
||||
fileCount: 0,
|
||||
canOpen: false,
|
||||
canRename: false,
|
||||
canShare: false,
|
||||
trash: true,
|
||||
onRestore: onRestore ? () => void onRestore(selectedList.map((r) => r.name)) : undefined,
|
||||
onOpen: () => {},
|
||||
onDownload: () => {},
|
||||
onShare: () => {},
|
||||
onRename: () => {},
|
||||
onCut: () => {},
|
||||
onCopy: () => {},
|
||||
onDelete: () => deleteNames(selectedList.map((r) => r.name)),
|
||||
onClear: onClearSelection,
|
||||
pluginFiles: [],
|
||||
} : selectedList.length > 0 ? {
|
||||
count: selectedList.length,
|
||||
fileCount: selectedList.filter((r) => !r.isDirectory && !r.isSymlink).length,
|
||||
canOpen: !!singleSelected && !singleSelected.isSymlink
|
||||
@@ -1110,6 +1193,7 @@ export function FileBrowser({
|
||||
compact={isMobile}
|
||||
toolsHidden={accountPickerMode}
|
||||
selection={selectionActions}
|
||||
emptyTrash={inTrash && onEmptyTrash ? { onEmpty: onEmptyTrash, disabled: trashCount === 0 } : undefined}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface FileViewProps {
|
||||
iconOptions: IconOptions;
|
||||
getImageUrl: (name: string) => Promise<string>;
|
||||
handlers: FileItemHandlers;
|
||||
/** A line about an item, as its tooltip (where a deleted item came from). */
|
||||
itemNote?: (resource: FileResource) => string | undefined;
|
||||
}
|
||||
|
||||
const LONG_PRESS_MS = 450;
|
||||
@@ -138,7 +140,7 @@ const LIST_COLUMNS = "grid grid-cols-[auto_minmax(0,1fr)_auto] sm:grid-cols-[aut
|
||||
|
||||
export function FileListView({
|
||||
resources, selected, focusedName, dropTarget, cutNames, starredPaths, pathOf, touch, iconOptions, getImageUrl, handlers,
|
||||
sortKey, sortDir, onSort, allSelected, someSelected, onToggleAll,
|
||||
sortKey, sortDir, onSort, allSelected, someSelected, onToggleAll, itemNote, dateLabel,
|
||||
}: FileViewProps & {
|
||||
sortKey: SortKey;
|
||||
sortDir: SortDir;
|
||||
@@ -146,6 +148,8 @@ export function FileListView({
|
||||
allSelected: boolean;
|
||||
someSelected: boolean;
|
||||
onToggleAll: () => void;
|
||||
/** The date column's heading, "Modified" unless told otherwise. */
|
||||
dateLabel?: string;
|
||||
}) {
|
||||
const t = useTranslations("files");
|
||||
const selecting = selected.size > 0;
|
||||
@@ -165,7 +169,7 @@ export function FileListView({
|
||||
{someSelected && !allSelected && <span className="sr-only">{t("selected_count", { count: selected.size })}</span>}
|
||||
</div>
|
||||
<SortHeader label={t("name")} column="name" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
|
||||
<SortHeader label={t("modified")} column="modified" sortKey={sortKey} sortDir={sortDir} onSort={onSort} className="hidden md:block" />
|
||||
<SortHeader label={dateLabel ?? t("modified")} column="modified" sortKey={sortKey} sortDir={sortDir} onSort={onSort} className="hidden md:block" />
|
||||
<SortHeader label={t("size")} column="size" sortKey={sortKey} sortDir={sortDir} onSort={onSort} className="hidden sm:block text-end" />
|
||||
<div role="columnheader" className="w-[4.75rem]"><span className="sr-only">{t("context_menu")}</span></div>
|
||||
</div>
|
||||
@@ -183,13 +187,14 @@ export function FileListView({
|
||||
iconOptions={iconOptions}
|
||||
getImageUrl={getImageUrl}
|
||||
handlers={handlers}
|
||||
note={itemNote?.(resource)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isStarred, selecting, touch, iconOptions, getImageUrl, handlers }: {
|
||||
function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isStarred, selecting, touch, iconOptions, getImageUrl, handlers, note }: {
|
||||
resource: FileResource;
|
||||
isSelected: boolean;
|
||||
isFocused: boolean;
|
||||
@@ -201,6 +206,7 @@ function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isS
|
||||
iconOptions: IconOptions;
|
||||
getImageUrl: (name: string) => Promise<string>;
|
||||
handlers: FileItemHandlers;
|
||||
note?: string;
|
||||
}) {
|
||||
const t = useTranslations("files");
|
||||
const touchProps = useTouchPress(touch, selecting, resource, handlers);
|
||||
@@ -251,7 +257,7 @@ function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isS
|
||||
}}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
className={cn("truncate text-start hover:underline underline-offset-2 decoration-muted-foreground/50", resource.isDirectory && "font-medium")}
|
||||
title={resource.name}
|
||||
title={note ? `${resource.name}\n${note}` : resource.name}
|
||||
>
|
||||
{resource.name}
|
||||
</button>
|
||||
@@ -260,6 +266,7 @@ function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isS
|
||||
</div>
|
||||
{touch && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{note && <>{note} · </>}
|
||||
{formatShortDate(resource.lastModified)}
|
||||
{!resource.isDirectory && !resource.isSymlink && <> · {formatFileSize(resource.contentLength)}</>}
|
||||
</div>
|
||||
|
||||
+115
-43
@@ -7,7 +7,8 @@ import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
||||
import { useAuthStore, redirectToLogin, saveRedirectAfterLogin } from '@/stores/auth-store';
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { useFileStore, isSharedLocation, linkedPreviewResource, resourceServerRef, resourceToView, type UploadReport } from "@/stores/file-store";
|
||||
import { useFileStore, deletesToTrash, isSharedLocation, linkedPreviewResource, resourceServerRef, resourceToView, type UploadReport } from "@/stores/file-store";
|
||||
import { isTrashLocation } from "@/lib/files-trash";
|
||||
import { fileErrorKind, isPartialCopy } from "@/lib/file-errors";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { cn, formatFileSize } from "@/lib/utils";
|
||||
@@ -109,6 +110,11 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
|
||||
lastAction,
|
||||
shareResource,
|
||||
openSharedNode,
|
||||
trashResources,
|
||||
trashNode,
|
||||
restoreResources,
|
||||
purgeResources,
|
||||
emptyTrash,
|
||||
} = useFileStore();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -397,42 +403,6 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
|
||||
}
|
||||
}, [uploadFolder, t, maxSizeUpload, showUploadReport, failureReason]);
|
||||
|
||||
const handleDelete = useCallback(async (name: string) => {
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("delete_confirm_title"),
|
||||
message: t("delete_confirm_message", { name }),
|
||||
confirmText: t("delete"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
if (!await deleteResource(name)) return;
|
||||
toast.success(t("delete_success"));
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
}, [deleteResource, confirmDialog, t, failureReason]);
|
||||
|
||||
const handleBatchDelete = useCallback(async (names: string[]) => {
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("delete_confirm_title"),
|
||||
message: t("batch_delete_confirm_message", { count: names.length }),
|
||||
confirmText: t("delete"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
if (!await deleteResources(names)) return;
|
||||
toast.success(t("batch_delete_success", { count: names.length }));
|
||||
} catch (err) {
|
||||
console.error("Failed to batch delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
}, [deleteResources, confirmDialog, t, failureReason]);
|
||||
|
||||
const handleUndo = useCallback(async () => {
|
||||
try {
|
||||
await undoLastAction();
|
||||
@@ -443,6 +413,93 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
|
||||
}
|
||||
}, [undoLastAction, t, failureReason]);
|
||||
|
||||
// Delete (R3): in the own drive items go to the trash, with Undo and no
|
||||
// question; in the trash and in a drive someone shared they are gone for
|
||||
// good, so that asks first.
|
||||
const currentParentIdForDelete = useFileStore((s) => s.currentParentId);
|
||||
const handleDeleteNames = useCallback(async (names: string[]) => {
|
||||
if (names.length === 0) return;
|
||||
if (isTrashLocation(currentParentIdForDelete)) {
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("delete_forever_title", { count: names.length }),
|
||||
message: names.length === 1
|
||||
? t("delete_forever_message", { name: names[0] })
|
||||
: t("delete_forever_message_count", { count: names.length }),
|
||||
confirmText: t("delete_forever"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const count = await purgeResources(names);
|
||||
if (count > 0) toast.success(t("deleted_forever", { count }));
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (deletesToTrash(currentParentIdForDelete)) {
|
||||
try {
|
||||
const count = await trashResources(names);
|
||||
if (count > 0) {
|
||||
toast.success(t("moved_to_trash", { count }), {
|
||||
action: { label: t("undo"), onClick: handleUndo },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("delete_forever_title", { count: names.length }),
|
||||
message: names.length === 1
|
||||
? t("delete_forever_message", { name: names[0] })
|
||||
: t("delete_forever_message_count", { count: names.length }),
|
||||
confirmText: t("delete_forever"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
if (names.length === 1 ? !await deleteResource(names[0]) : !await deleteResources(names)) return;
|
||||
toast.success(t("deleted_forever", { count: names.length }));
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
}, [currentParentIdForDelete, confirmDialog, t, purgeResources, trashResources, deleteResource, deleteResources, handleUndo, failureReason]);
|
||||
const handleDelete = useCallback((name: string) => handleDeleteNames([name]), [handleDeleteNames]);
|
||||
const handleBatchDelete = handleDeleteNames;
|
||||
|
||||
const handleRestore = useCallback(async (names: string[]) => {
|
||||
try {
|
||||
const count = await restoreResources(names);
|
||||
if (count > 0) toast.success(t("restored", { count }));
|
||||
} catch (err) {
|
||||
console.error("Failed to restore:", err);
|
||||
toast.error(t("restore_error"), failureReason(err));
|
||||
}
|
||||
}, [restoreResources, t, failureReason]);
|
||||
|
||||
const trashCount = useFileStore((s) => s.trashCount);
|
||||
const handleEmptyTrash = useCallback(async () => {
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("empty_trash_title"),
|
||||
message: t("empty_trash_message", { count: trashCount }),
|
||||
confirmText: t("empty_trash"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await emptyTrash();
|
||||
toast.success(t("trash_emptied"));
|
||||
} catch (err) {
|
||||
console.error("Failed to empty the trash:", err);
|
||||
toast.error(t("empty_trash_error"), failureReason(err));
|
||||
}
|
||||
}, [confirmDialog, t, trashCount, emptyTrash, failureReason]);
|
||||
|
||||
const handleRename = useCallback(async (oldName: string, newName: string) => {
|
||||
try {
|
||||
if (!await renameResource(oldName, newName)) return;
|
||||
@@ -546,21 +603,34 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
|
||||
}, [renameNode, t, handleUndo]);
|
||||
|
||||
const handleDeleteNode = useCallback(async (id: string, name: string) => {
|
||||
// A folder of the own drive goes to the trash; one of a shared drive is gone for good.
|
||||
if (deletesToTrash(id)) {
|
||||
try {
|
||||
if (!await trashNode(id)) return;
|
||||
toast.success(t("moved_to_trash", { count: 1 }), {
|
||||
action: { label: t("undo"), onClick: handleUndo },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirmDialog({
|
||||
title: t("delete_confirm_title"),
|
||||
message: t("delete_confirm_message", { name }),
|
||||
confirmText: t("delete"),
|
||||
title: t("delete_forever_title", { count: 1 }),
|
||||
message: t("delete_forever_message", { name }),
|
||||
confirmText: t("delete_forever"),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
if (!await deleteNode(id)) return;
|
||||
toast.success(t("delete_success"));
|
||||
toast.success(t("deleted_forever", { count: 1 }));
|
||||
} catch (err) {
|
||||
console.error("Failed to delete:", err);
|
||||
toast.error(t("delete_error"));
|
||||
toast.error(t("delete_error"), failureReason(err));
|
||||
}
|
||||
}, [deleteNode, confirmDialog, t]);
|
||||
}, [deleteNode, trashNode, confirmDialog, handleUndo, t, failureReason]);
|
||||
|
||||
const handleDownloadNode = useCallback(async (id: string) => {
|
||||
try {
|
||||
@@ -716,6 +786,8 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
|
||||
onCancelUpload={cancelUpload}
|
||||
onDelete={handleDelete}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onRestore={handleRestore}
|
||||
onEmptyTrash={() => void handleEmptyTrash()}
|
||||
onRename={handleRename}
|
||||
onDownload={handleDownload}
|
||||
onBatchDownload={handleBatchDownload}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Menu, ChevronRight, Search, X, ArrowUpDown, Check, LayoutGrid, LayoutList,
|
||||
PanelRight, Download, Trash2, Scissors, Copy, Pencil, Share2, Clipboard,
|
||||
MoreHorizontal, FolderOpen, Folder, ArrowUp, ArrowDown, RefreshCw,
|
||||
MoreHorizontal, FolderOpen, Folder, ArrowUp, ArrowDown, RefreshCw, RotateCcw,
|
||||
} from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu";
|
||||
@@ -45,6 +45,9 @@ interface SelectionActions {
|
||||
pluginFiles: FileResourceView[];
|
||||
/** The one selected item's type, so plugins can offer type-specific actions; undefined for several. */
|
||||
pluginMimeType?: string;
|
||||
/** Items in the trash: Restore and Delete permanently, nothing else. */
|
||||
trash?: boolean;
|
||||
onRestore?: () => void;
|
||||
}
|
||||
|
||||
interface FilesHeaderProps {
|
||||
@@ -74,6 +77,8 @@ interface FilesHeaderProps {
|
||||
/** Hide the tools that only apply to a folder listing (Pro account picker). */
|
||||
toolsHidden?: boolean;
|
||||
selection: SelectionActions | null;
|
||||
/** In the trash: the Empty trash button. */
|
||||
emptyTrash?: { onEmpty: () => void; disabled: boolean };
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_CRUMBS = 3;
|
||||
@@ -103,6 +108,7 @@ export function FilesHeader({
|
||||
compact,
|
||||
toolsHidden,
|
||||
selection,
|
||||
emptyTrash,
|
||||
}: FilesHeaderProps) {
|
||||
const t = useTranslations("files");
|
||||
const sortMenu = useContextMenu<null>();
|
||||
@@ -122,6 +128,15 @@ export function FilesHeader({
|
||||
{t("selected_count", { count: selection.count })}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 min-w-0 overflow-x-auto">
|
||||
{selection.trash ? (
|
||||
<>
|
||||
{selection.onRestore && (
|
||||
<ToolButton icon={RotateCcw} label={t("restore")} onClick={selection.onRestore} compact={compact} />
|
||||
)}
|
||||
<ToolButton icon={Trash2} label={t("delete_forever")} onClick={selection.onDelete} compact destructive />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{selection.canOpen && (
|
||||
<ToolButton icon={FolderOpen} label={t("open")} onClick={selection.onOpen} compact={compact} />
|
||||
)}
|
||||
@@ -138,9 +153,11 @@ export function FilesHeader({
|
||||
<ToolButton icon={Copy} label={t("copy")} onClick={selection.onCopy} compact />
|
||||
<ToolButton icon={Trash2} label={t("delete")} onClick={selection.onDelete} compact destructive />
|
||||
{!compact && <SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Outside the scrolling row, so the "more" button never scrolls out of sight. */}
|
||||
{compact && <SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} overflow />}
|
||||
{compact && !selection.trash && <SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} overflow />}
|
||||
<div className="flex-1" />
|
||||
<DetailsToggle showDetails={showDetails} onToggle={onToggleDetails} label={t("details")} />
|
||||
</div>
|
||||
@@ -281,6 +298,19 @@ export function FilesHeader({
|
||||
|
||||
{!toolsHidden && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{emptyTrash && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-destructive hover:text-destructive"
|
||||
onClick={emptyTrash.onEmpty}
|
||||
disabled={emptyTrash.disabled}
|
||||
title={t("empty_trash")}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span className={cn(compact && "sr-only")}>{t("empty_trash")}</span>
|
||||
</Button>
|
||||
)}
|
||||
{clipboardCount > 0 && (
|
||||
<Button variant="outline" size="sm" className="h-8 gap-1.5" onClick={onPaste} title={t("paste_here")}>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Plus, HardDrive, File, Folder, FolderOpen, ChevronRight, ChevronDown, Star, StarOff,
|
||||
Clock, Share2, Upload, FolderUp, FolderPlus, FilePlus, Pencil, Trash2, Download, Eye, X, FolderSearch,
|
||||
} from "@/components/icons";
|
||||
import { isTrashLocation } from "@/lib/files-trash";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu";
|
||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||
@@ -77,6 +78,11 @@ export interface FilesSidebarProps {
|
||||
onRevealRecent: (id: string) => void;
|
||||
onDownloadRecent: (id: string) => void;
|
||||
onRemoveRecent: (id: string) => void;
|
||||
/** Items in the trash; the entry shows their number. */
|
||||
trashCount?: number;
|
||||
onOpenTrash?: () => void;
|
||||
/** Delete items dragged onto the trash entry. */
|
||||
onDropToTrash?: (names: string[]) => void;
|
||||
}
|
||||
|
||||
export function FilesSidebar({
|
||||
@@ -103,6 +109,9 @@ export function FilesSidebar({
|
||||
onRevealRecent,
|
||||
onDownloadRecent,
|
||||
onRemoveRecent,
|
||||
trashCount = 0,
|
||||
onOpenTrash,
|
||||
onDropToTrash,
|
||||
}: FilesSidebarProps) {
|
||||
const t = useTranslations("files");
|
||||
const newMenu = useContextMenu<null>();
|
||||
@@ -386,6 +395,37 @@ export function FilesSidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onOpenTrash && (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenTrash}
|
||||
aria-current={isTrashLocation(currentParentId) ? "page" : undefined}
|
||||
className={rowClass(isTrashLocation(currentParentId), dropTarget === "__trash__")}
|
||||
style={{ ...ROW_STYLE, paddingInlineStart: "12px" }}
|
||||
onDragOver={(e) => {
|
||||
if (!isInternalDrag(e) || !onDropToTrash || isTrashLocation(currentParentId)) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
setDropTarget("__trash__");
|
||||
}}
|
||||
onDragLeave={() => setDropTarget((cur) => (cur === "__trash__" ? null : cur))}
|
||||
onDrop={(e) => {
|
||||
if (!isInternalDrag(e) || !onDropToTrash) return;
|
||||
e.preventDefault();
|
||||
setDropTarget(null);
|
||||
const names = readDraggedNames(e);
|
||||
if (names.length > 0) onDropToTrash(names);
|
||||
}}
|
||||
>
|
||||
<span className="w-3.5 -ms-1 shrink-0" />
|
||||
<Trash2 className="w-4 h-4 shrink-0" />
|
||||
<span className="truncate flex-1">{t("trash")}</span>
|
||||
{trashCount > 0 && <span className="text-xs text-muted-foreground tabular-nums">{trashCount}</span>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recent.length === 0 && favorites.length === 0 && (
|
||||
<p className="px-4 pt-4 text-xs text-muted-foreground flex items-start gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5 shrink-0 mt-px" />
|
||||
|
||||
@@ -1408,7 +1408,18 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
}
|
||||
|
||||
async destroyFileNodes(ids: string[]): Promise<{ destroyed: string[]; notDestroyed: string[] }> {
|
||||
// Like the server: a folder goes with everything in it (emptying the trash).
|
||||
const idSet = new Set(ids);
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const n of this.data.fileNodes) {
|
||||
if (n.parentId && idSet.has(n.parentId) && !idSet.has(n.id)) {
|
||||
idSet.add(n.id);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.data.fileNodes = this.data.fileNodes.filter(n => !idSet.has(n.id));
|
||||
return { destroyed: ids, notDestroyed: [] };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { FileNode } from '@/lib/jmap/types';
|
||||
import { fileNodeKind } from '@/lib/jmap/filenode-kind';
|
||||
|
||||
/*
|
||||
* The Files trash. Neither Stalwart 0.16 nor 1.0 keeps deleted FileNodes:
|
||||
* FileNode/set destroy removes a node and everything below it for good (1.0
|
||||
* can label a folder `role: "trash"`, 0.16 ignores the role, and neither moves
|
||||
* anything there). So the trash is a folder the app manages: `.Trash` at the
|
||||
* root, hidden from every listing. Each delete puts its items into a folder
|
||||
* of its own inside it, named after when it happened and where the items came
|
||||
* from, so Restore can put them back and two deleted files of the same name
|
||||
* never clash:
|
||||
*
|
||||
* /.Trash/1730112000000~<parentId>/report.pdf
|
||||
*
|
||||
* The name is the record, so a WebDAV client or another device sees the same
|
||||
* trash, and nothing else has to be stored.
|
||||
*/
|
||||
|
||||
export const TRASH_FOLDER_NAME = '.Trash';
|
||||
/** The trash as a place in the Files view. JMAP ids never contain "*". */
|
||||
export const TRASH_LOCATION = '*trash';
|
||||
/** The trash's path, for links and the breadcrumb. */
|
||||
export const TRASH_PATH = `/${TRASH_FOLDER_NAME}`;
|
||||
|
||||
export function isTrashLocation(id: string | null | undefined): boolean {
|
||||
return id === TRASH_LOCATION;
|
||||
}
|
||||
|
||||
/** The trash folder of the account's own drive, if there is one yet. */
|
||||
export function findTrashFolder(nodes: FileNode[]): FileNode | undefined {
|
||||
return nodes.find(n =>
|
||||
(n.parentId ?? null) === null
|
||||
&& n.name === TRASH_FOLDER_NAME
|
||||
&& !n.isShared
|
||||
&& !n.id.includes(':')
|
||||
&& fileNodeKind(n) === 'directory');
|
||||
}
|
||||
|
||||
/** The name of the folder one delete's items go into. */
|
||||
export function trashBatchName(parentId: string | null, deletedAt: number = Date.now()): string {
|
||||
return `${deletedAt}~${parentId ?? ''}`;
|
||||
}
|
||||
|
||||
/** When a batch was deleted and from which folder (null: the root); null for any other name. */
|
||||
export function parseTrashBatchName(name: string): { deletedAt: number; parentId: string | null } | null {
|
||||
const match = /^(\d{10,})~([A-Za-z0-9_-]*)$/.exec(name);
|
||||
if (!match) return null;
|
||||
return { deletedAt: Number(match[1]), parentId: match[2] || null };
|
||||
}
|
||||
|
||||
/** The trash folder and everything in it, to keep out of normal listings. */
|
||||
export function trashSubtreeIds(nodes: FileNode[], trashId: string | undefined): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
if (!trashId) return ids;
|
||||
ids.add(trashId);
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const n of nodes) {
|
||||
if (n.parentId && ids.has(n.parentId) && !ids.has(n.id)) {
|
||||
ids.add(n.id);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
export interface TrashedItem {
|
||||
node: FileNode;
|
||||
/** The delete's folder in the trash; null for something put into the trash directly. */
|
||||
batchId: string | null;
|
||||
deletedAt: number | null;
|
||||
/** Where it came from; null for the root or when unknown. */
|
||||
fromParentId: string | null;
|
||||
}
|
||||
|
||||
/** What the trash holds, one entry per deleted item. */
|
||||
export function trashedItems(nodes: FileNode[], trash: FileNode | undefined): TrashedItem[] {
|
||||
if (!trash) return [];
|
||||
const items: TrashedItem[] = [];
|
||||
for (const child of nodes.filter(n => n.parentId === trash.id)) {
|
||||
const batch = fileNodeKind(child) === 'directory' ? parseTrashBatchName(child.name) : null;
|
||||
if (!batch) {
|
||||
items.push({ node: child, batchId: null, deletedAt: null, fromParentId: null });
|
||||
continue;
|
||||
}
|
||||
for (const node of nodes.filter(n => n.parentId === child.id)) {
|
||||
items.push({ node, batchId: child.id, deletedAt: batch.deletedAt, fromParentId: batch.parentId });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "لا يمكن أن تحتوي الأسماء على أحرف تحكم.",
|
||||
"name_too_long": "هذا الاسم طويل جدًا بالنسبة للخادم.",
|
||||
"delete": "حذف",
|
||||
"delete_confirm_title": "حذف المورد",
|
||||
"delete_confirm_message": "هل أنت متأكد أنك تريد حذف \"{name}\"؟ لا يمكن التراجع عن هذا.",
|
||||
"download": "تنزيل",
|
||||
"name": "الاسم",
|
||||
"size": "الحجم",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "بقي جزء من النسخة في مكانه.",
|
||||
"create_folder_success": "تم إنشاء المجلد",
|
||||
"create_folder_error": "فشل إنشاء المجلد",
|
||||
"delete_success": "تم الحذف بنجاح",
|
||||
"delete_error": "فشل الحذف",
|
||||
"trash": "سلة المهملات",
|
||||
"restore": "استعادة",
|
||||
"restored": "{count, plural, one {تمت استعادة العنصر} other {تمت استعادة # عنصر}}",
|
||||
"restore_error": "تعذّرت الاستعادة",
|
||||
"moved_to_trash": "{count, plural, one {نُقل إلى سلة المهملات} other {نُقل # عنصر إلى سلة المهملات}}",
|
||||
"delete_forever": "حذف نهائي",
|
||||
"delete_forever_title": "{count, plural, one {حذف نهائي؟} other {حذف # عنصر نهائيًا؟}}",
|
||||
"delete_forever_message": "سيُحذف \"{name}\". لا يمكنك التراجع عن ذلك.",
|
||||
"delete_forever_message_count": "{count, plural, one {سيُحذف عنصر واحد} other {سيُحذف # عنصر}}. لا يمكنك التراجع عن ذلك.",
|
||||
"deleted_forever": "{count, plural, one {حُذف نهائيًا} other {حُذف # عنصر نهائيًا}}",
|
||||
"empty_trash": "إفراغ سلة المهملات",
|
||||
"empty_trash_title": "إفراغ سلة المهملات؟",
|
||||
"empty_trash_message": "{count, plural, one {سيُحذف العنصر الموجود في سلة المهملات} other {ستُحذف جميع العناصر (#) في سلة المهملات}} نهائيًا. لا يمكنك التراجع عن ذلك.",
|
||||
"trash_emptied": "أُفرغت سلة المهملات",
|
||||
"empty_trash_error": "تعذّر إفراغ سلة المهملات",
|
||||
"trash_empty_title": "سلة المهملات فارغة",
|
||||
"trash_empty_hint": "تبقى الملفات المحذوفة هنا حتى تُفرغ سلة المهملات.",
|
||||
"deleted": "تاريخ الحذف",
|
||||
"deleted_from": "حُذف من {folder}",
|
||||
"rename_success": "تمت إعادة التسمية بنجاح",
|
||||
"rename_error": "فشلت إعادة التسمية",
|
||||
"download_error": "فشل التنزيل",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "إنشاء",
|
||||
"save": "حفظ",
|
||||
"no_results": "لا توجد ملفات مطابقة لبحثك",
|
||||
"batch_delete_confirm_message": "هل أنت متأكد أنك تريد حذف {count, plural, one {عنصر واحد} other {# عنصر}}؟ لا يمكن التراجع عن هذا.",
|
||||
"batch_delete_success": "{count, plural, one {تم حذف عنصر واحد} other {تم حذف # عنصر}}",
|
||||
"grid_view": "عرض شبكي",
|
||||
"list_view": "عرض قائمة",
|
||||
"details": "التفاصيل",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Els noms no poden contenir caràcters de control.",
|
||||
"name_too_long": "Aquest nom és massa llarg per al servidor.",
|
||||
"delete": "Suprimeix",
|
||||
"delete_confirm_title": "Suprimeix el recurs",
|
||||
"delete_confirm_message": "Segur que voleu suprimir «{name}»? Això no es pot desfer.",
|
||||
"download": "Baixa",
|
||||
"name": "Nom",
|
||||
"size": "Mida",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "Part de la còpia s'ha quedat al seu lloc.",
|
||||
"create_folder_success": "Carpeta creada",
|
||||
"create_folder_error": "No s'ha pogut crear la carpeta",
|
||||
"delete_success": "Suprimit correctament",
|
||||
"delete_error": "No s'ha pogut suprimir",
|
||||
"trash": "Paperera",
|
||||
"restore": "Restaura",
|
||||
"restored": "{count, plural, one {S'ha restaurat l'element} other {S'han restaurat # elements}}",
|
||||
"restore_error": "No s'ha pogut restaurar",
|
||||
"moved_to_trash": "{count, plural, one {S'ha mogut a la paperera} other {S'han mogut # elements a la paperera}}",
|
||||
"delete_forever": "Suprimeix definitivament",
|
||||
"delete_forever_title": "{count, plural, one {Voleu suprimir-lo definitivament?} other {Voleu suprimir # elements definitivament?}}",
|
||||
"delete_forever_message": "Se suprimirà «{name}». No es pot desfer.",
|
||||
"delete_forever_message_count": "{count, plural, one {Se suprimirà 1 element} other {Se suprimiran # elements}}. No es pot desfer.",
|
||||
"deleted_forever": "{count, plural, one {S'ha suprimit definitivament} other {S'han suprimit # elements definitivament}}",
|
||||
"empty_trash": "Buida la paperera",
|
||||
"empty_trash_title": "Voleu buidar la paperera?",
|
||||
"empty_trash_message": "{count, plural, one {L'element de la paperera se suprimirà} other {Els # elements de la paperera se suprimiran}} definitivament. No es pot desfer.",
|
||||
"trash_emptied": "S'ha buidat la paperera",
|
||||
"empty_trash_error": "No s'ha pogut buidar la paperera",
|
||||
"trash_empty_title": "La paperera és buida",
|
||||
"trash_empty_hint": "Els fitxers suprimits es queden aquí fins que buideu la paperera.",
|
||||
"deleted": "Suprimit",
|
||||
"deleted_from": "Suprimit de {folder}",
|
||||
"rename_success": "Nom canviat correctament",
|
||||
"rename_error": "No s'ha pogut canviar el nom",
|
||||
"download_error": "No s'ha pogut baixar",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Crea",
|
||||
"save": "Desa",
|
||||
"no_results": "Cap fitxer coincideix amb la cerca",
|
||||
"batch_delete_confirm_message": "Segur que voleu suprimir {count, plural, one {1 element} other {# elements}}? Això no es pot desfer.",
|
||||
"batch_delete_success": "{count, plural, one {1 element suprimit} other {# elements suprimits}}",
|
||||
"grid_view": "Vista de quadrícula",
|
||||
"list_view": "Vista de llista",
|
||||
"details": "Detalls",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Názvy nesmí obsahovat řídicí znaky.",
|
||||
"name_too_long": "Tento název je pro server příliš dlouhý.",
|
||||
"delete": "Odstranit",
|
||||
"delete_confirm_title": "Odstranit položku",
|
||||
"delete_confirm_message": "Opravdu chcete odstranit \"{name}\"? Tuto akci nelze vrátit zpět.",
|
||||
"download": "Stáhnout",
|
||||
"name": "Název",
|
||||
"size": "Velikost",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Část kopie zůstala na místě.",
|
||||
"create_folder_success": "Složka byla vytvořena",
|
||||
"create_folder_error": "Vytvoření složky selhalo",
|
||||
"delete_success": "Úspěšně odstraněno",
|
||||
"delete_error": "Odstranění selhalo",
|
||||
"trash": "Koš",
|
||||
"restore": "Obnovit",
|
||||
"restored": "{count, plural, one {Položka obnovena} few {Obnoveny # položky} other {Obnoveno # položek}}",
|
||||
"restore_error": "Obnovení se nezdařilo",
|
||||
"moved_to_trash": "{count, plural, one {Přesunuto do koše} few {# položky přesunuty do koše} other {# položek přesunuto do koše}}",
|
||||
"delete_forever": "Trvale smazat",
|
||||
"delete_forever_title": "{count, plural, one {Trvale smazat?} few {Trvale smazat # položky?} other {Trvale smazat # položek?}}",
|
||||
"delete_forever_message": "Položka „{name}“ bude smazána. Tuto akci nelze vrátit.",
|
||||
"delete_forever_message_count": "{count, plural, one {Bude smazána 1 položka} few {Budou smazány # položky} other {Bude smazáno # položek}}. Tuto akci nelze vrátit.",
|
||||
"deleted_forever": "{count, plural, one {Trvale smazáno} few {Trvale smazány # položky} other {Trvale smazáno # položek}}",
|
||||
"empty_trash": "Vysypat koš",
|
||||
"empty_trash_title": "Vysypat koš?",
|
||||
"empty_trash_message": "{count, plural, one {Položka v koši bude trvale smazána} few {Všechny # položky v koši budou trvale smazány} other {Všech # položek v koši bude trvale smazáno}}. Tuto akci nelze vrátit.",
|
||||
"trash_emptied": "Koš byl vysypán",
|
||||
"empty_trash_error": "Koš se nepodařilo vysypat",
|
||||
"trash_empty_title": "Koš je prázdný",
|
||||
"trash_empty_hint": "Smazané soubory zůstávají zde, dokud koš nevysypete.",
|
||||
"deleted": "Smazáno",
|
||||
"deleted_from": "Smazáno z {folder}",
|
||||
"rename_success": "Úspěšně přejmenováno",
|
||||
"rename_error": "Přejmenování selhalo",
|
||||
"download_error": "Stahování selhalo",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Vytvořit",
|
||||
"save": "Uložit",
|
||||
"no_results": "Hledání neodpovídají žádné soubory",
|
||||
"batch_delete_confirm_message": "Opravdu chcete odstranit {count, plural, one {1 položku} few {# položky} other {# položek}}? Tuto akci nelze vrátit zpět.",
|
||||
"batch_delete_success": "{count, plural, one {Odstraněna 1 položka} few {Odstraněny # položky} other {Odstraněno # položek}}",
|
||||
"grid_view": "Zobrazení mřížky",
|
||||
"list_view": "Zobrazení seznamu",
|
||||
"details": "Podrobnosti",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Navne må ikke indeholde kontroltegn.",
|
||||
"name_too_long": "Dette navn er for langt til serveren.",
|
||||
"delete": "Slet",
|
||||
"delete_confirm_title": "Slet ressource",
|
||||
"delete_confirm_message": "Er du sikker på, at du vil slette \"{name}\"? Dette kan ikke fortrydes.",
|
||||
"download": "Download",
|
||||
"name": "Navn",
|
||||
"size": "Størrelse",
|
||||
@@ -3905,8 +3903,26 @@
|
||||
"error_partial_copy": "En del af kopien blev liggende.",
|
||||
"create_folder_success": "Mappe oprettet",
|
||||
"create_folder_error": "Kunne ikke oprette mappe",
|
||||
"delete_success": "Slettet",
|
||||
"delete_error": "Kunne ikke slette",
|
||||
"trash": "Papirkurv",
|
||||
"restore": "Gendan",
|
||||
"restored": "{count, plural, one {Elementet er gendannet} other {# elementer er gendannet}}",
|
||||
"restore_error": "Kunne ikke gendanne",
|
||||
"moved_to_trash": "{count, plural, one {Flyttet til papirkurven} other {# elementer flyttet til papirkurven}}",
|
||||
"delete_forever": "Slet permanent",
|
||||
"delete_forever_title": "{count, plural, one {Slet permanent?} other {Slet # elementer permanent?}}",
|
||||
"delete_forever_message": "\"{name}\" bliver slettet. Det kan ikke fortrydes.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 element bliver slettet} other {# elementer bliver slettet}}. Det kan ikke fortrydes.",
|
||||
"deleted_forever": "{count, plural, one {Slettet permanent} other {# elementer slettet permanent}}",
|
||||
"empty_trash": "Tøm papirkurven",
|
||||
"empty_trash_title": "Tøm papirkurven?",
|
||||
"empty_trash_message": "{count, plural, one {Elementet i papirkurven} other {Alle # elementer i papirkurven}} bliver slettet permanent. Det kan ikke fortrydes.",
|
||||
"trash_emptied": "Papirkurven er tømt",
|
||||
"empty_trash_error": "Kunne ikke tømme papirkurven",
|
||||
"trash_empty_title": "Papirkurven er tom",
|
||||
"trash_empty_hint": "Slettede filer bliver her, indtil du tømmer papirkurven.",
|
||||
"deleted": "Slettet",
|
||||
"deleted_from": "Slettet fra {folder}",
|
||||
"rename_success": "Omdøbt",
|
||||
"rename_error": "Kunne ikke omdøbe",
|
||||
"download_error": "Kunne ikke downloade",
|
||||
@@ -3915,8 +3931,6 @@
|
||||
"create": "Opret",
|
||||
"save": "Gem",
|
||||
"no_results": "Ingen filer matcher din søgning",
|
||||
"batch_delete_confirm_message": "Er du sikker på, at du vil slette {count, plural, one {1 element} other {# elementer}}? Dette kan ikke fortrydes.",
|
||||
"batch_delete_success": "{count, plural, one {1 element slettet} other {# elementer slettet}}",
|
||||
"grid_view": "Gittervisning",
|
||||
"list_view": "Listevisning",
|
||||
"details": "Detaljer",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Namen dürfen keine Steuerzeichen enthalten.",
|
||||
"name_too_long": "Dieser Name ist für den Server zu lang.",
|
||||
"delete": "Löschen",
|
||||
"delete_confirm_title": "Ressource löschen",
|
||||
"delete_confirm_message": "Möchten Sie \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"download": "Herunterladen",
|
||||
"name": "Name",
|
||||
"size": "Größe",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Ein Teil der Kopie wurde nicht entfernt.",
|
||||
"create_folder_success": "Ordner erstellt",
|
||||
"create_folder_error": "Ordner konnte nicht erstellt werden",
|
||||
"delete_success": "Erfolgreich gelöscht",
|
||||
"delete_error": "Löschen fehlgeschlagen",
|
||||
"trash": "Papierkorb",
|
||||
"restore": "Wiederherstellen",
|
||||
"restored": "{count, plural, one {Element wiederhergestellt} other {# Elemente wiederhergestellt}}",
|
||||
"restore_error": "Wiederherstellen fehlgeschlagen",
|
||||
"moved_to_trash": "{count, plural, one {In den Papierkorb verschoben} other {# Elemente in den Papierkorb verschoben}}",
|
||||
"delete_forever": "Endgültig löschen",
|
||||
"delete_forever_title": "{count, plural, one {Endgültig löschen?} other {# Elemente endgültig löschen?}}",
|
||||
"delete_forever_message": "„{name}“ wird gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 Element wird gelöscht} other {# Elemente werden gelöscht}}. Das lässt sich nicht rückgängig machen.",
|
||||
"deleted_forever": "{count, plural, one {Endgültig gelöscht} other {# Elemente endgültig gelöscht}}",
|
||||
"empty_trash": "Papierkorb leeren",
|
||||
"empty_trash_title": "Papierkorb leeren?",
|
||||
"empty_trash_message": "{count, plural, one {Das Element im Papierkorb wird} other {Alle # Elemente im Papierkorb werden}} endgültig gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"trash_emptied": "Papierkorb geleert",
|
||||
"empty_trash_error": "Papierkorb konnte nicht geleert werden",
|
||||
"trash_empty_title": "Der Papierkorb ist leer",
|
||||
"trash_empty_hint": "Gelöschte Dateien bleiben hier, bis Sie den Papierkorb leeren.",
|
||||
"deleted": "Gelöscht",
|
||||
"deleted_from": "Gelöscht aus {folder}",
|
||||
"rename_success": "Erfolgreich umbenannt",
|
||||
"rename_error": "Umbenennen fehlgeschlagen",
|
||||
"download_error": "Herunterladen fehlgeschlagen",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Erstellen",
|
||||
"save": "Speichern",
|
||||
"no_results": "Keine Dateien entsprechen Ihrer Suche",
|
||||
"batch_delete_confirm_message": "Möchten Sie wirklich {count, plural, one {1 Element} other {# Elemente}} löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"batch_delete_success": "{count, plural, one {1 Element gelöscht} other {# Elemente gelöscht}}",
|
||||
"grid_view": "Rasteransicht",
|
||||
"list_view": "Listenansicht",
|
||||
"details": "Details",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Names cannot contain control characters.",
|
||||
"name_too_long": "This name is too long for the server.",
|
||||
"delete": "Delete",
|
||||
"delete_confirm_title": "Delete resource",
|
||||
"delete_confirm_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||
"download": "Download",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "Part of the copy was left in place.",
|
||||
"create_folder_success": "Folder created",
|
||||
"create_folder_error": "Failed to create folder",
|
||||
"delete_success": "Deleted successfully",
|
||||
"delete_error": "Failed to delete",
|
||||
"trash": "Trash",
|
||||
"restore": "Restore",
|
||||
"restored": "{count, plural, one {Item restored} other {# items restored}}",
|
||||
"restore_error": "Couldn't restore",
|
||||
"moved_to_trash": "{count, plural, one {Moved to trash} other {# items moved to trash}}",
|
||||
"delete_forever": "Delete permanently",
|
||||
"delete_forever_title": "{count, plural, one {Delete permanently?} other {Delete # items permanently?}}",
|
||||
"delete_forever_message": "\"{name}\" will be deleted. You can't undo this.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 item} other {# items}} will be deleted. You can't undo this.",
|
||||
"deleted_forever": "{count, plural, one {Deleted permanently} other {# items deleted permanently}}",
|
||||
"empty_trash": "Empty trash",
|
||||
"empty_trash_title": "Empty trash?",
|
||||
"empty_trash_message": "{count, plural, one {The item in the trash} other {All # items in the trash}} will be deleted permanently. You can't undo this.",
|
||||
"trash_emptied": "Trash emptied",
|
||||
"empty_trash_error": "Couldn't empty the trash",
|
||||
"trash_empty_title": "Trash is empty",
|
||||
"trash_empty_hint": "Deleted files stay here until you empty the trash.",
|
||||
"deleted": "Deleted",
|
||||
"deleted_from": "Deleted from {folder}",
|
||||
"rename_success": "Renamed successfully",
|
||||
"rename_error": "Failed to rename",
|
||||
"download_error": "Failed to download",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Create",
|
||||
"save": "Save",
|
||||
"no_results": "No files match your search",
|
||||
"batch_delete_confirm_message": "Are you sure you want to delete {count, plural, one {1 item} other {# items}}? This cannot be undone.",
|
||||
"batch_delete_success": "{count, plural, one {1 item deleted} other {# items deleted}}",
|
||||
"grid_view": "Grid view",
|
||||
"list_view": "List view",
|
||||
"details": "Details",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Los nombres no pueden contener caracteres de control.",
|
||||
"name_too_long": "Este nombre es demasiado largo para el servidor.",
|
||||
"delete": "Eliminar",
|
||||
"delete_confirm_title": "Eliminar recurso",
|
||||
"delete_confirm_message": "¿Está seguro de que desea eliminar \"{name}\"? Esta acción no se puede deshacer.",
|
||||
"download": "Descargar",
|
||||
"name": "Nombre",
|
||||
"size": "Tamaño",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Una parte de la copia se ha quedado en su lugar.",
|
||||
"create_folder_success": "Carpeta creada",
|
||||
"create_folder_error": "Error al crear la carpeta",
|
||||
"delete_success": "Eliminado correctamente",
|
||||
"delete_error": "Error al eliminar",
|
||||
"trash": "Papelera",
|
||||
"restore": "Restaurar",
|
||||
"restored": "{count, plural, one {Elemento restaurado} other {# elementos restaurados}}",
|
||||
"restore_error": "No se ha podido restaurar",
|
||||
"moved_to_trash": "{count, plural, one {Movido a la papelera} other {# elementos movidos a la papelera}}",
|
||||
"delete_forever": "Eliminar definitivamente",
|
||||
"delete_forever_title": "{count, plural, one {¿Eliminar definitivamente?} other {¿Eliminar # elementos definitivamente?}}",
|
||||
"delete_forever_message": "Se eliminará «{name}». No se puede deshacer.",
|
||||
"delete_forever_message_count": "{count, plural, one {Se eliminará 1 elemento} other {Se eliminarán # elementos}}. No se puede deshacer.",
|
||||
"deleted_forever": "{count, plural, one {Eliminado definitivamente} other {# elementos eliminados definitivamente}}",
|
||||
"empty_trash": "Vaciar la papelera",
|
||||
"empty_trash_title": "¿Vaciar la papelera?",
|
||||
"empty_trash_message": "{count, plural, one {El elemento de la papelera se eliminará} other {Los # elementos de la papelera se eliminarán}} definitivamente. No se puede deshacer.",
|
||||
"trash_emptied": "Papelera vaciada",
|
||||
"empty_trash_error": "No se ha podido vaciar la papelera",
|
||||
"trash_empty_title": "La papelera está vacía",
|
||||
"trash_empty_hint": "Los archivos eliminados se quedan aquí hasta que vacíes la papelera.",
|
||||
"deleted": "Eliminado",
|
||||
"deleted_from": "Eliminado de {folder}",
|
||||
"rename_success": "Renombrado correctamente",
|
||||
"rename_error": "Error al renombrar",
|
||||
"download_error": "Error al descargar",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Crear",
|
||||
"save": "Guardar",
|
||||
"no_results": "Ningún archivo coincide con su búsqueda",
|
||||
"batch_delete_confirm_message": "¿Está seguro de que desea eliminar {count, plural, one {1 elemento} other {# elementos}}? Esta acción no se puede deshacer.",
|
||||
"batch_delete_success": "{count, plural, one {1 elemento eliminado} other {# elementos eliminados}}",
|
||||
"grid_view": "Vista de cuadrícula",
|
||||
"list_view": "Vista de lista",
|
||||
"details": "Detalles",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Nimed ei tohi sisaldada juhtmärke.",
|
||||
"name_too_long": "See nimi on serveri jaoks liiga pikk.",
|
||||
"delete": "Kustuta",
|
||||
"delete_confirm_title": "Kustuta ressurss",
|
||||
"delete_confirm_message": "Kas oled kindel, et soovid kustutada \"{name}\"? Seda ei saa tagasi võtta.",
|
||||
"download": "Laadi alla",
|
||||
"name": "Nimi",
|
||||
"size": "Suurus",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "Osa koopiast jäi alles.",
|
||||
"create_folder_success": "Kaust loodud",
|
||||
"create_folder_error": "Kausta loomine ebaõnnestus",
|
||||
"delete_success": "Edukalt kustutatud",
|
||||
"delete_error": "Kustutamine ebaõnnestus",
|
||||
"trash": "Prügikast",
|
||||
"restore": "Taasta",
|
||||
"restored": "{count, plural, one {Üksus taastati} other {# üksust taastati}}",
|
||||
"restore_error": "Taastamine ebaõnnestus",
|
||||
"moved_to_trash": "{count, plural, one {Teisaldati prügikasti} other {# üksust teisaldati prügikasti}}",
|
||||
"delete_forever": "Kustuta jäädavalt",
|
||||
"delete_forever_title": "{count, plural, one {Kas kustutada jäädavalt?} other {Kas kustutada # üksust jäädavalt?}}",
|
||||
"delete_forever_message": "„{name}“ kustutatakse. Seda ei saa tagasi võtta.",
|
||||
"delete_forever_message_count": "{count, plural, one {Kustutatakse 1 üksus} other {Kustutatakse # üksust}}. Seda ei saa tagasi võtta.",
|
||||
"deleted_forever": "{count, plural, one {Kustutati jäädavalt} other {# üksust kustutati jäädavalt}}",
|
||||
"empty_trash": "Tühjenda prügikast",
|
||||
"empty_trash_title": "Kas tühjendada prügikast?",
|
||||
"empty_trash_message": "{count, plural, one {Prügikastis olev üksus} other {Kõik # prügikastis olevat üksust}} kustutatakse jäädavalt. Seda ei saa tagasi võtta.",
|
||||
"trash_emptied": "Prügikast tühjendati",
|
||||
"empty_trash_error": "Prügikasti ei õnnestunud tühjendada",
|
||||
"trash_empty_title": "Prügikast on tühi",
|
||||
"trash_empty_hint": "Kustutatud failid jäävad siia, kuni prügikasti tühjendate.",
|
||||
"deleted": "Kustutatud",
|
||||
"deleted_from": "Kustutatud kaustast {folder}",
|
||||
"rename_success": "Edukalt ümber nimetatud",
|
||||
"rename_error": "Ümbernimetamine ebaõnnestus",
|
||||
"download_error": "Allalaadimine ebaõnnestus",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Loo",
|
||||
"save": "Salvesta",
|
||||
"no_results": "Otsingule vastavaid faile ei leitud",
|
||||
"batch_delete_confirm_message": "Kas oled kindel, et soovid kustutada {count, plural, one {1 üksuse} other {# üksust}}? Seda ei saa tagasi võtta.",
|
||||
"batch_delete_success": "{count, plural, one {1 üksus kustutatud} other {# üksust kustutatud}}",
|
||||
"grid_view": "Ruudustikvaade",
|
||||
"list_view": "Loendivaade",
|
||||
"details": "Üksikasjad",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "نامها نمیتوانند نویسههای کنترلی داشته باشند.",
|
||||
"name_too_long": "این نام برای سرور بیش از حد طولانی است.",
|
||||
"delete": "حذف",
|
||||
"delete_confirm_title": "حذف منبع",
|
||||
"delete_confirm_message": "آیا از حذف \"{name}\" اطمینان دارید؟ این عملیات قابل بازگشت نیست.",
|
||||
"download": "دانلود",
|
||||
"name": "نام",
|
||||
"size": "حجم",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "بخشی از رونوشت در جای خود باقی ماند.",
|
||||
"create_folder_success": "پوشه ایجاد شد",
|
||||
"create_folder_error": "ایجاد پوشه ناموفق بود",
|
||||
"delete_success": "با موفقیت حذف شد",
|
||||
"delete_error": "حذف ناموفق بود",
|
||||
"trash": "سطل زباله",
|
||||
"restore": "بازیابی",
|
||||
"restored": "{count, plural, one {مورد بازیابی شد} other {# مورد بازیابی شد}}",
|
||||
"restore_error": "بازیابی انجام نشد",
|
||||
"moved_to_trash": "{count, plural, one {به سطل زباله منتقل شد} other {# مورد به سطل زباله منتقل شد}}",
|
||||
"delete_forever": "حذف دائمی",
|
||||
"delete_forever_title": "{count, plural, one {برای همیشه حذف شود؟} other {# مورد برای همیشه حذف شود؟}}",
|
||||
"delete_forever_message": "«{name}» حذف میشود. این کار قابل بازگشت نیست.",
|
||||
"delete_forever_message_count": "{count, plural, one {۱ مورد حذف میشود} other {# مورد حذف میشود}}. این کار قابل بازگشت نیست.",
|
||||
"deleted_forever": "{count, plural, one {برای همیشه حذف شد} other {# مورد برای همیشه حذف شد}}",
|
||||
"empty_trash": "خالی کردن سطل زباله",
|
||||
"empty_trash_title": "سطل زباله خالی شود؟",
|
||||
"empty_trash_message": "{count, plural, one {مورد داخل سطل زباله} other {همه # مورد داخل سطل زباله}} برای همیشه حذف میشود. این کار قابل بازگشت نیست.",
|
||||
"trash_emptied": "سطل زباله خالی شد",
|
||||
"empty_trash_error": "سطل زباله خالی نشد",
|
||||
"trash_empty_title": "سطل زباله خالی است",
|
||||
"trash_empty_hint": "فایلهای حذفشده تا زمانی که سطل زباله را خالی کنید اینجا میمانند.",
|
||||
"deleted": "حذفشده",
|
||||
"deleted_from": "حذفشده از {folder}",
|
||||
"rename_success": "تغییر نام با موفقیت انجام شد",
|
||||
"rename_error": "تغییر نام ناموفق بود",
|
||||
"download_error": "دانلود ناموفق بود",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "ایجاد",
|
||||
"save": "ذخیره",
|
||||
"no_results": "فایلی با این جستجو مطابقت ندارد",
|
||||
"batch_delete_confirm_message": "آیا از حذف {count, plural, one {۱ مورد} other {# مورد}} اطمینان دارید؟",
|
||||
"batch_delete_success": "{count, plural, one {۱ مورد حذف شد} other {# مورد حذف شد}}",
|
||||
"grid_view": "نمای شبکهای",
|
||||
"list_view": "نمای لیست",
|
||||
"details": "جزئیات",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Les noms ne peuvent pas contenir de caractères de contrôle.",
|
||||
"name_too_long": "Ce nom est trop long pour le serveur.",
|
||||
"delete": "Supprimer",
|
||||
"delete_confirm_title": "Supprimer la ressource",
|
||||
"delete_confirm_message": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.",
|
||||
"download": "Télécharger",
|
||||
"name": "Nom",
|
||||
"size": "Taille",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Une partie de la copie est restée en place.",
|
||||
"create_folder_success": "Dossier créé",
|
||||
"create_folder_error": "Échec de la création du dossier",
|
||||
"delete_success": "Supprimé avec succès",
|
||||
"delete_error": "Échec de la suppression",
|
||||
"trash": "Corbeille",
|
||||
"restore": "Restaurer",
|
||||
"restored": "{count, plural, one {Élément restauré} other {# éléments restaurés}}",
|
||||
"restore_error": "Impossible de restaurer",
|
||||
"moved_to_trash": "{count, plural, one {Placé dans la corbeille} other {# éléments placés dans la corbeille}}",
|
||||
"delete_forever": "Supprimer définitivement",
|
||||
"delete_forever_title": "{count, plural, one {Supprimer définitivement ?} other {Supprimer définitivement # éléments ?}}",
|
||||
"delete_forever_message": "« {name} » sera supprimé. Cette action est irréversible.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 élément sera supprimé} other {# éléments seront supprimés}}. Cette action est irréversible.",
|
||||
"deleted_forever": "{count, plural, one {Supprimé définitivement} other {# éléments supprimés définitivement}}",
|
||||
"empty_trash": "Vider la corbeille",
|
||||
"empty_trash_title": "Vider la corbeille ?",
|
||||
"empty_trash_message": "{count, plural, one {L'élément de la corbeille sera supprimé} other {Les # éléments de la corbeille seront supprimés}} définitivement. Cette action est irréversible.",
|
||||
"trash_emptied": "Corbeille vidée",
|
||||
"empty_trash_error": "Impossible de vider la corbeille",
|
||||
"trash_empty_title": "La corbeille est vide",
|
||||
"trash_empty_hint": "Les fichiers supprimés restent ici jusqu'à ce que vous vidiez la corbeille.",
|
||||
"deleted": "Supprimé le",
|
||||
"deleted_from": "Supprimé de {folder}",
|
||||
"rename_success": "Renommé avec succès",
|
||||
"rename_error": "Échec du renommage",
|
||||
"download_error": "Échec du téléchargement",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Créer",
|
||||
"save": "Enregistrer",
|
||||
"no_results": "Aucun fichier ne correspond à votre recherche",
|
||||
"batch_delete_confirm_message": "Êtes-vous sûr de vouloir supprimer {count, plural, one {1 élément} other {# éléments}} ? Cette action est irréversible.",
|
||||
"batch_delete_success": "{count, plural, one {1 élément supprimé} other {# éléments supprimés}}",
|
||||
"grid_view": "Vue en grille",
|
||||
"list_view": "Vue en liste",
|
||||
"details": "Détails",
|
||||
|
||||
+19
-5
@@ -3750,8 +3750,6 @@
|
||||
"name_control_chars": "שמות אינם יכולים להכיל תווי בקרה.",
|
||||
"name_too_long": "השם הזה ארוך מדי עבור השרת.",
|
||||
"delete": "לִמְחוֹק",
|
||||
"delete_confirm_title": "מחק משאב",
|
||||
"delete_confirm_message": "האם אתה בטוח שברצונך למחוק את \"{name}\"? לא ניתן לבטל זאת.",
|
||||
"download": "הורד",
|
||||
"name": "שֵׁם",
|
||||
"size": "גודל",
|
||||
@@ -3776,8 +3774,26 @@
|
||||
"error_partial_copy": "חלק מההעתק נשאר במקומו.",
|
||||
"create_folder_success": "התיקיה נוצרה",
|
||||
"create_folder_error": "יצירת התיקיה נכשלה",
|
||||
"delete_success": "נמחק בהצלחה",
|
||||
"delete_error": "המחיקה נכשלה",
|
||||
"trash": "אשפה",
|
||||
"restore": "שחזר",
|
||||
"restored": "{count, plural, one {הפריט שוחזר} other {# פריטים שוחזרו}}",
|
||||
"restore_error": "לא ניתן היה לשחזר",
|
||||
"moved_to_trash": "{count, plural, one {הועבר לאשפה} other {# פריטים הועברו לאשפה}}",
|
||||
"delete_forever": "מחק לצמיתות",
|
||||
"delete_forever_title": "{count, plural, one {למחוק לצמיתות?} other {למחוק # פריטים לצמיתות?}}",
|
||||
"delete_forever_message": "\"{name}\" יימחק. לא ניתן לבטל פעולה זו.",
|
||||
"delete_forever_message_count": "{count, plural, one {פריט אחד יימחק} other {# פריטים יימחקו}}. לא ניתן לבטל פעולה זו.",
|
||||
"deleted_forever": "{count, plural, one {נמחק לצמיתות} other {# פריטים נמחקו לצמיתות}}",
|
||||
"empty_trash": "רוקן את האשפה",
|
||||
"empty_trash_title": "לרוקן את האשפה?",
|
||||
"empty_trash_message": "{count, plural, one {הפריט באשפה יימחק} other {כל # הפריטים באשפה יימחקו}} לצמיתות. לא ניתן לבטל פעולה זו.",
|
||||
"trash_emptied": "האשפה רוקנה",
|
||||
"empty_trash_error": "לא ניתן היה לרוקן את האשפה",
|
||||
"trash_empty_title": "האשפה ריקה",
|
||||
"trash_empty_hint": "קבצים שנמחקו נשארים כאן עד שתרוקן את האשפה.",
|
||||
"deleted": "נמחק",
|
||||
"deleted_from": "נמחק מ-{folder}",
|
||||
"rename_success": "השם שונה בהצלחה",
|
||||
"rename_error": "שינוי השם נכשל",
|
||||
"download_error": "ההורדה נכשלה",
|
||||
@@ -3786,8 +3802,6 @@
|
||||
"create": "צור",
|
||||
"save": "שמור",
|
||||
"no_results": "אין קבצים שתואמים לחיפוש שלך",
|
||||
"batch_delete_confirm_message": "האם למחוק {count, plural, one {פריט אחד} other {# פריטים}}? לא ניתן לבטל זאת.",
|
||||
"batch_delete_success": "{count, plural, one {פריט אחד נמחק} other {# פריטים נמחקו}}",
|
||||
"grid_view": "תצוגת רשת",
|
||||
"list_view": "תצוגת רשימה",
|
||||
"details": "פרטים",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "A nevek nem tartalmazhatnak vezérlőkaraktereket.",
|
||||
"name_too_long": "Ez a név túl hosszú a kiszolgáló számára.",
|
||||
"delete": "Törlés",
|
||||
"delete_confirm_title": "Erőforrás törlése",
|
||||
"delete_confirm_message": "Biztosan törölni szeretnéd a \"{name}\" elemet? Ez nem vonható vissza.",
|
||||
"download": "Letöltés",
|
||||
"name": "Név",
|
||||
"size": "Méret",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "A másolat egy része a helyén maradt.",
|
||||
"create_folder_success": "Mappa létrehozva",
|
||||
"create_folder_error": "Nem sikerült létrehozni a mappát",
|
||||
"delete_success": "Sikeresen törölve",
|
||||
"delete_error": "Nem sikerült törölni",
|
||||
"trash": "Kuka",
|
||||
"restore": "Visszaállítás",
|
||||
"restored": "{count, plural, one {Elem visszaállítva} other {# elem visszaállítva}}",
|
||||
"restore_error": "Nem sikerült visszaállítani",
|
||||
"moved_to_trash": "{count, plural, one {Áthelyezve a kukába} other {# elem áthelyezve a kukába}}",
|
||||
"delete_forever": "Végleges törlés",
|
||||
"delete_forever_title": "{count, plural, one {Véglegesen törli?} other {Véglegesen törli a(z) # elemet?}}",
|
||||
"delete_forever_message": "A(z) „{name}” törlődik. Ez nem vonható vissza.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 elem törlődik} other {# elem törlődik}}. Ez nem vonható vissza.",
|
||||
"deleted_forever": "{count, plural, one {Véglegesen törölve} other {# elem véglegesen törölve}}",
|
||||
"empty_trash": "Kuka ürítése",
|
||||
"empty_trash_title": "Kiüríti a kukát?",
|
||||
"empty_trash_message": "{count, plural, one {A kukában lévő elem} other {A kukában lévő mind a(z) # elem}} véglegesen törlődik. Ez nem vonható vissza.",
|
||||
"trash_emptied": "Kuka kiürítve",
|
||||
"empty_trash_error": "Nem sikerült kiüríteni a kukát",
|
||||
"trash_empty_title": "A kuka üres",
|
||||
"trash_empty_hint": "A törölt fájlok itt maradnak, amíg ki nem üríti a kukát.",
|
||||
"deleted": "Törölve",
|
||||
"deleted_from": "Törölve innen: {folder}",
|
||||
"rename_success": "Sikeresen átnevezve",
|
||||
"rename_error": "Nem sikerült átnevezni",
|
||||
"download_error": "Nem sikerült letölteni",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Létrehozás",
|
||||
"save": "Mentés",
|
||||
"no_results": "Nincs a keresésnek megfelelő fájl",
|
||||
"batch_delete_confirm_message": "Biztosan törölni szeretnéd {count, plural, one {az 1 elemet} other {a # elemet}}? Ez nem vonható vissza.",
|
||||
"batch_delete_success": "{count, plural, one {1 elem törölve} other {# elem törölve}}",
|
||||
"grid_view": "Rács nézet",
|
||||
"list_view": "Lista nézet",
|
||||
"details": "Részletek",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "I nomi non possono contenere caratteri di controllo.",
|
||||
"name_too_long": "Questo nome è troppo lungo per il server.",
|
||||
"delete": "Elimina",
|
||||
"delete_confirm_title": "Elimina risorsa",
|
||||
"delete_confirm_message": "Sei sicuro di voler eliminare \"{name}\"? Questa azione non può essere annullata.",
|
||||
"download": "Scarica",
|
||||
"name": "Nome",
|
||||
"size": "Dimensione",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Una parte della copia è rimasta al suo posto.",
|
||||
"create_folder_success": "Cartella creata",
|
||||
"create_folder_error": "Creazione della cartella non riuscita",
|
||||
"delete_success": "Eliminato con successo",
|
||||
"delete_error": "Eliminazione non riuscita",
|
||||
"trash": "Cestino",
|
||||
"restore": "Ripristina",
|
||||
"restored": "{count, plural, one {Elemento ripristinato} other {# elementi ripristinati}}",
|
||||
"restore_error": "Impossibile ripristinare",
|
||||
"moved_to_trash": "{count, plural, one {Spostato nel cestino} other {# elementi spostati nel cestino}}",
|
||||
"delete_forever": "Elimina definitivamente",
|
||||
"delete_forever_title": "{count, plural, one {Eliminare definitivamente?} other {Eliminare definitivamente # elementi?}}",
|
||||
"delete_forever_message": "«{name}» verrà eliminato. L'operazione non può essere annullata.",
|
||||
"delete_forever_message_count": "{count, plural, one {Verrà eliminato 1 elemento} other {Verranno eliminati # elementi}}. L'operazione non può essere annullata.",
|
||||
"deleted_forever": "{count, plural, one {Eliminato definitivamente} other {# elementi eliminati definitivamente}}",
|
||||
"empty_trash": "Svuota il cestino",
|
||||
"empty_trash_title": "Svuotare il cestino?",
|
||||
"empty_trash_message": "{count, plural, one {L'elemento nel cestino verrà eliminato} other {Tutti i # elementi nel cestino verranno eliminati}} definitivamente. L'operazione non può essere annullata.",
|
||||
"trash_emptied": "Cestino svuotato",
|
||||
"empty_trash_error": "Impossibile svuotare il cestino",
|
||||
"trash_empty_title": "Il cestino è vuoto",
|
||||
"trash_empty_hint": "I file eliminati restano qui finché non svuoti il cestino.",
|
||||
"deleted": "Eliminato",
|
||||
"deleted_from": "Eliminato da {folder}",
|
||||
"rename_success": "Rinominato con successo",
|
||||
"rename_error": "Rinominazione non riuscita",
|
||||
"download_error": "Download non riuscito",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Crea",
|
||||
"save": "Salva",
|
||||
"no_results": "Nessun file corrisponde alla ricerca",
|
||||
"batch_delete_confirm_message": "Sei sicuro di voler eliminare {count, plural, one {1 elemento} other {# elementi}}? Questa azione non può essere annullata.",
|
||||
"batch_delete_success": "{count, plural, one {1 elemento eliminato} other {# elementi eliminati}}",
|
||||
"grid_view": "Vista a griglia",
|
||||
"list_view": "Vista a elenco",
|
||||
"details": "Dettagli",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "名前に制御文字は使用できません。",
|
||||
"name_too_long": "この名前はサーバーには長すぎます。",
|
||||
"delete": "削除",
|
||||
"delete_confirm_title": "リソースを削除",
|
||||
"delete_confirm_message": "\"{name}\"を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"download": "ダウンロード",
|
||||
"name": "名前",
|
||||
"size": "サイズ",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "コピーの一部は削除されずに残っています。",
|
||||
"create_folder_success": "フォルダーを作成しました",
|
||||
"create_folder_error": "フォルダーの作成に失敗しました",
|
||||
"delete_success": "正常に削除しました",
|
||||
"delete_error": "削除に失敗しました",
|
||||
"trash": "ゴミ箱",
|
||||
"restore": "復元",
|
||||
"restored": "{count, plural, other {#件のアイテムを復元しました}}",
|
||||
"restore_error": "復元できませんでした",
|
||||
"moved_to_trash": "{count, plural, other {#件のアイテムをゴミ箱に移動しました}}",
|
||||
"delete_forever": "完全に削除",
|
||||
"delete_forever_title": "{count, plural, other {#件のアイテムを完全に削除しますか?}}",
|
||||
"delete_forever_message": "「{name}」は削除されます。この操作は元に戻せません。",
|
||||
"delete_forever_message_count": "{count, plural, other {#件のアイテムが削除されます}}。この操作は元に戻せません。",
|
||||
"deleted_forever": "{count, plural, other {#件のアイテムを完全に削除しました}}",
|
||||
"empty_trash": "ゴミ箱を空にする",
|
||||
"empty_trash_title": "ゴミ箱を空にしますか?",
|
||||
"empty_trash_message": "{count, plural, other {ゴミ箱内の#件のアイテムがすべて完全に削除されます}}。この操作は元に戻せません。",
|
||||
"trash_emptied": "ゴミ箱を空にしました",
|
||||
"empty_trash_error": "ゴミ箱を空にできませんでした",
|
||||
"trash_empty_title": "ゴミ箱は空です",
|
||||
"trash_empty_hint": "削除したファイルはゴミ箱を空にするまでここに残ります。",
|
||||
"deleted": "削除日",
|
||||
"deleted_from": "{folder} から削除",
|
||||
"rename_success": "正常に名前を変更しました",
|
||||
"rename_error": "名前の変更に失敗しました",
|
||||
"download_error": "ダウンロードに失敗しました",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "作成",
|
||||
"save": "保存",
|
||||
"no_results": "検索に一致するファイルがありません",
|
||||
"batch_delete_confirm_message": "{count, plural, other {#件のアイテム}}を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"batch_delete_success": "{count, plural, other {#件のアイテムを削除しました}}",
|
||||
"grid_view": "グリッド表示",
|
||||
"list_view": "リスト表示",
|
||||
"details": "詳細",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "이름에 제어 문자를 사용할 수 없습니다.",
|
||||
"name_too_long": "이 이름은 서버에서 사용하기에 너무 깁니다.",
|
||||
"delete": "삭제",
|
||||
"delete_confirm_title": "항목 삭제",
|
||||
"delete_confirm_message": "정말 \"{name}\"을(를) 삭제할까요? 이 작업은 되돌릴 수 없어요.",
|
||||
"download": "다운로드",
|
||||
"name": "이름",
|
||||
"size": "크기",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "복사본의 일부가 그대로 남아 있어요.",
|
||||
"create_folder_success": "폴더가 만들어졌어요",
|
||||
"create_folder_error": "폴더를 만들지 못했어요",
|
||||
"delete_success": "성공적으로 삭제되었어요",
|
||||
"delete_error": "삭제에 실패했어요",
|
||||
"trash": "휴지통",
|
||||
"restore": "복원",
|
||||
"restored": "{count, plural, other {항목 #개를 복원했습니다}}",
|
||||
"restore_error": "복원하지 못했습니다",
|
||||
"moved_to_trash": "{count, plural, other {항목 #개를 휴지통으로 이동했습니다}}",
|
||||
"delete_forever": "영구 삭제",
|
||||
"delete_forever_title": "{count, plural, other {항목 #개를 영구 삭제할까요?}}",
|
||||
"delete_forever_message": "\"{name}\" 항목이 삭제됩니다. 되돌릴 수 없습니다.",
|
||||
"delete_forever_message_count": "{count, plural, other {항목 #개가 삭제됩니다}}. 되돌릴 수 없습니다.",
|
||||
"deleted_forever": "{count, plural, other {항목 #개를 영구 삭제했습니다}}",
|
||||
"empty_trash": "휴지통 비우기",
|
||||
"empty_trash_title": "휴지통을 비울까요?",
|
||||
"empty_trash_message": "{count, plural, other {휴지통의 항목 #개가 모두 영구 삭제됩니다}}. 되돌릴 수 없습니다.",
|
||||
"trash_emptied": "휴지통을 비웠습니다",
|
||||
"empty_trash_error": "휴지통을 비우지 못했습니다",
|
||||
"trash_empty_title": "휴지통이 비어 있습니다",
|
||||
"trash_empty_hint": "삭제한 파일은 휴지통을 비울 때까지 여기에 남아 있습니다.",
|
||||
"deleted": "삭제한 날짜",
|
||||
"deleted_from": "{folder}에서 삭제됨",
|
||||
"rename_success": "이름이 변경되었어요",
|
||||
"rename_error": "이름을 바꾸지 못했어요",
|
||||
"download_error": "다운로드에 실패했어요",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "만들기",
|
||||
"save": "저장",
|
||||
"no_results": "검색된 파일이 없어요",
|
||||
"batch_delete_confirm_message": "선택한 {count}개의 항목을 정말 삭제할까요? 이 작업은 되돌릴 수 없어요.",
|
||||
"batch_delete_success": "{count}개의 항목이 삭제되었어요",
|
||||
"grid_view": "그리드 뷰",
|
||||
"list_view": "리스트 뷰",
|
||||
"details": "세부 정보",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Nosaukumos nedrīkst būt vadības rakstzīmes.",
|
||||
"name_too_long": "Šis nosaukums serverim ir pārāk garš.",
|
||||
"delete": "Dzēst",
|
||||
"delete_confirm_title": "Dzēst resursu",
|
||||
"delete_confirm_message": "Vai tiešām vēlaties dzēst «{name}»? Šo darbību nevar atcelt.",
|
||||
"download": "Lejupielādēt",
|
||||
"name": "Nosaukums",
|
||||
"size": "Izmērs",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Daļa kopijas palika savā vietā.",
|
||||
"create_folder_success": "Mape izveidota",
|
||||
"create_folder_error": "Neizdevās izveidot mapi",
|
||||
"delete_success": "Sekmīgi izdzēsts",
|
||||
"delete_error": "Neizdevās izdzēst",
|
||||
"trash": "Miskaste",
|
||||
"restore": "Atjaunot",
|
||||
"restored": "{count, plural, one {Vienums atjaunots} other {Atjaunoti # vienumi}}",
|
||||
"restore_error": "Neizdevās atjaunot",
|
||||
"moved_to_trash": "{count, plural, one {Pārvietots uz miskasti} other {# vienumi pārvietoti uz miskasti}}",
|
||||
"delete_forever": "Dzēst neatgriezeniski",
|
||||
"delete_forever_title": "{count, plural, one {Dzēst neatgriezeniski?} other {Dzēst # vienumus neatgriezeniski?}}",
|
||||
"delete_forever_message": "“{name}” tiks dzēsts. To nevar atsaukt.",
|
||||
"delete_forever_message_count": "{count, plural, one {Tiks dzēsts 1 vienums} other {Tiks dzēsti # vienumi}}. To nevar atsaukt.",
|
||||
"deleted_forever": "{count, plural, one {Dzēsts neatgriezeniski} other {# vienumi dzēsti neatgriezeniski}}",
|
||||
"empty_trash": "Iztukšot miskasti",
|
||||
"empty_trash_title": "Iztukšot miskasti?",
|
||||
"empty_trash_message": "{count, plural, one {Miskastē esošais vienums tiks dzēsts} other {Visi # miskastē esošie vienumi tiks dzēsti}} neatgriezeniski. To nevar atsaukt.",
|
||||
"trash_emptied": "Miskaste iztukšota",
|
||||
"empty_trash_error": "Neizdevās iztukšot miskasti",
|
||||
"trash_empty_title": "Miskaste ir tukša",
|
||||
"trash_empty_hint": "Dzēstie faili paliek šeit, līdz iztukšojat miskasti.",
|
||||
"deleted": "Dzēsts",
|
||||
"deleted_from": "Dzēsts no {folder}",
|
||||
"rename_success": "Sekmīgi pārdēvēts",
|
||||
"rename_error": "Neizdevās pārdēvēt",
|
||||
"download_error": "Neizdevās lejupielādēt",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Izveidot",
|
||||
"save": "Saglabāt",
|
||||
"no_results": "Nav failu, kas atbilstu vaicājumam",
|
||||
"batch_delete_confirm_message": "Vai tiešām vēlaties dzēst {count, plural, one {1 elementu} other {# elementus}}?",
|
||||
"batch_delete_success": "Izdzēsts {count, plural, one {1 elements} other {# elementi}}",
|
||||
"grid_view": "Režģis",
|
||||
"list_view": "Saraksts",
|
||||
"details": "Sīkāka informācija",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Нэрэнд удирдах тэмдэгт агуулж болохгүй.",
|
||||
"name_too_long": "Энэ нэр серверийн хувьд хэт урт байна.",
|
||||
"delete": "Устгах",
|
||||
"delete_confirm_title": "Нөөцийг устгах",
|
||||
"delete_confirm_message": "Та \"{name}\"-г устгахдаа итгэлтэй байна уу? Үүнийг буцаах боломжгүй.",
|
||||
"download": "Татаж авах",
|
||||
"name": "Нэр",
|
||||
"size": "Хэмжээ",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "Хуулбарын нэг хэсэг хэвээр үлдлээ.",
|
||||
"create_folder_success": "Хавтас үүсгэсэн",
|
||||
"create_folder_error": "Хавтас үүсгэж чадсангүй",
|
||||
"delete_success": "Амжилттай устгасан",
|
||||
"delete_error": "Устгаж чадсангүй",
|
||||
"trash": "Хогийн сав",
|
||||
"restore": "Сэргээх",
|
||||
"restored": "{count, plural, one {Зүйлийг сэргээлээ} other {# зүйлийг сэргээлээ}}",
|
||||
"restore_error": "Сэргээж чадсангүй",
|
||||
"moved_to_trash": "{count, plural, one {Хогийн сав руу зөөлөө} other {# зүйлийг хогийн сав руу зөөлөө}}",
|
||||
"delete_forever": "Бүрмөсөн устгах",
|
||||
"delete_forever_title": "{count, plural, one {Бүрмөсөн устгах уу?} other {# зүйлийг бүрмөсөн устгах уу?}}",
|
||||
"delete_forever_message": "\"{name}\" устгагдана. Үүнийг буцаах боломжгүй.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 зүйл устгагдана} other {# зүйл устгагдана}}. Үүнийг буцаах боломжгүй.",
|
||||
"deleted_forever": "{count, plural, one {Бүрмөсөн устгалаа} other {# зүйлийг бүрмөсөн устгалаа}}",
|
||||
"empty_trash": "Хогийн савыг хоослох",
|
||||
"empty_trash_title": "Хогийн савыг хоослох уу?",
|
||||
"empty_trash_message": "{count, plural, one {Хогийн саван дахь зүйл} other {Хогийн саван дахь бүх # зүйл}} бүрмөсөн устгагдана. Үүнийг буцаах боломжгүй.",
|
||||
"trash_emptied": "Хогийн савыг хоосоллоо",
|
||||
"empty_trash_error": "Хогийн савыг хоосолж чадсангүй",
|
||||
"trash_empty_title": "Хогийн сав хоосон байна",
|
||||
"trash_empty_hint": "Устгасан файлууд хогийн савыг хоослох хүртэл энд хадгалагдана.",
|
||||
"deleted": "Устгасан",
|
||||
"deleted_from": "{folder}-с устгасан",
|
||||
"rename_success": "Нэрээ амжилттай өөрчилсөн",
|
||||
"rename_error": "Нэрээ өөрчилж чадсангүй",
|
||||
"download_error": "Татаж авч чадсангүй",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Үүсгэх",
|
||||
"save": "Хадгалах",
|
||||
"no_results": "Таны хайлтад тохирох файл алга",
|
||||
"batch_delete_confirm_message": "{count, plural, one {1 элемент} other {# элемент}} устгахыг баталгаажуулж байна уу? Энэ үйлдлийг буцаах боломжгүй.",
|
||||
"batch_delete_success": "{count, plural, one {1 элемент устгагдлаа} other {# элемент устгагдлаа}}",
|
||||
"grid_view": "Сүлжээ харах",
|
||||
"list_view": "Жагсаалт харах",
|
||||
"details": "Дэлгэрэнгүй мэдээлэл",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Navn kan ikke inneholde kontrolltegn.",
|
||||
"name_too_long": "Dette navnet er for langt for serveren.",
|
||||
"delete": "Slett",
|
||||
"delete_confirm_title": "Slett fil eller mappe",
|
||||
"delete_confirm_message": "Er du sikker på at du vil slette «{name}»? Dette kan ikke angres.",
|
||||
"download": "Last ned",
|
||||
"name": "Navn",
|
||||
"size": "Størrelse",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "En del av kopien ble liggende igjen.",
|
||||
"create_folder_success": "Mappen er opprettet",
|
||||
"create_folder_error": "Kunne ikke opprette mappe",
|
||||
"delete_success": "Elementet er slettet",
|
||||
"delete_error": "Kunne ikke slette elementet",
|
||||
"trash": "Papirkurv",
|
||||
"restore": "Gjenopprett",
|
||||
"restored": "{count, plural, one {Elementet er gjenopprettet} other {# elementer er gjenopprettet}}",
|
||||
"restore_error": "Kunne ikke gjenopprette",
|
||||
"moved_to_trash": "{count, plural, one {Flyttet til papirkurven} other {# elementer flyttet til papirkurven}}",
|
||||
"delete_forever": "Slett permanent",
|
||||
"delete_forever_title": "{count, plural, one {Vil du slette permanent?} other {Vil du slette # elementer permanent?}}",
|
||||
"delete_forever_message": "«{name}» blir slettet. Dette kan ikke angres.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 element blir slettet} other {# elementer blir slettet}}. Dette kan ikke angres.",
|
||||
"deleted_forever": "{count, plural, one {Slettet permanent} other {# elementer slettet permanent}}",
|
||||
"empty_trash": "Tøm papirkurven",
|
||||
"empty_trash_title": "Vil du tømme papirkurven?",
|
||||
"empty_trash_message": "{count, plural, one {Elementet i papirkurven} other {Alle # elementer i papirkurven}} blir slettet permanent. Dette kan ikke angres.",
|
||||
"trash_emptied": "Papirkurven er tømt",
|
||||
"empty_trash_error": "Kunne ikke tømme papirkurven",
|
||||
"trash_empty_title": "Papirkurven er tom",
|
||||
"trash_empty_hint": "Slettede filer blir liggende her til du tømmer papirkurven.",
|
||||
"deleted": "Slettet",
|
||||
"deleted_from": "Slettet fra {folder}",
|
||||
"rename_success": "Navnet er endret",
|
||||
"rename_error": "Kunne ikke endre navnet",
|
||||
"download_error": "Kunne ikke laste ned",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Opprett",
|
||||
"save": "Lagre",
|
||||
"no_results": "Ingen filer passer med søket ditt",
|
||||
"batch_delete_confirm_message": "Er du sikker på at du vil slette {count, plural, one {1 element} other {# elementer}}? Dette kan ikke angres.",
|
||||
"batch_delete_success": "{count, plural, one {1 element slettet} other {# elementer slettet}}",
|
||||
"grid_view": "Rutenettvisning",
|
||||
"list_view": "Listevisning",
|
||||
"details": "Detaljer",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Namen mogen geen besturingstekens bevatten.",
|
||||
"name_too_long": "Deze naam is te lang voor de server.",
|
||||
"delete": "Verwijderen",
|
||||
"delete_confirm_title": "Bron verwijderen",
|
||||
"delete_confirm_message": "Weet u zeker dat u \"{name}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||
"download": "Downloaden",
|
||||
"name": "Naam",
|
||||
"size": "Grootte",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Een deel van de kopie is blijven staan.",
|
||||
"create_folder_success": "Map aangemaakt",
|
||||
"create_folder_error": "Map aanmaken mislukt",
|
||||
"delete_success": "Succesvol verwijderd",
|
||||
"delete_error": "Verwijderen mislukt",
|
||||
"trash": "Prullenbak",
|
||||
"restore": "Herstellen",
|
||||
"restored": "{count, plural, one {Item hersteld} other {# items hersteld}}",
|
||||
"restore_error": "Kan niet herstellen",
|
||||
"moved_to_trash": "{count, plural, one {Naar de prullenbak verplaatst} other {# items naar de prullenbak verplaatst}}",
|
||||
"delete_forever": "Definitief verwijderen",
|
||||
"delete_forever_title": "{count, plural, one {Definitief verwijderen?} other {# items definitief verwijderen?}}",
|
||||
"delete_forever_message": "'{name}' wordt verwijderd. Dit kan niet ongedaan worden gemaakt.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 item wordt verwijderd} other {# items worden verwijderd}}. Dit kan niet ongedaan worden gemaakt.",
|
||||
"deleted_forever": "{count, plural, one {Definitief verwijderd} other {# items definitief verwijderd}}",
|
||||
"empty_trash": "Prullenbak legen",
|
||||
"empty_trash_title": "Prullenbak legen?",
|
||||
"empty_trash_message": "{count, plural, one {Het item in de prullenbak wordt} other {Alle # items in de prullenbak worden}} definitief verwijderd. Dit kan niet ongedaan worden gemaakt.",
|
||||
"trash_emptied": "Prullenbak geleegd",
|
||||
"empty_trash_error": "Kan de prullenbak niet legen",
|
||||
"trash_empty_title": "De prullenbak is leeg",
|
||||
"trash_empty_hint": "Verwijderde bestanden blijven hier tot je de prullenbak leegt.",
|
||||
"deleted": "Verwijderd",
|
||||
"deleted_from": "Verwijderd uit {folder}",
|
||||
"rename_success": "Succesvol hernoemd",
|
||||
"rename_error": "Hernoemen mislukt",
|
||||
"download_error": "Downloaden mislukt",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Aanmaken",
|
||||
"save": "Opslaan",
|
||||
"no_results": "Geen bestanden komen overeen met uw zoekopdracht",
|
||||
"batch_delete_confirm_message": "Weet u zeker dat u {count, plural, one {1 item} other {# items}} wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||
"batch_delete_success": "{count, plural, one {1 item verwijderd} other {# items verwijderd}}",
|
||||
"grid_view": "Rasterweergave",
|
||||
"list_view": "Lijstweergave",
|
||||
"details": "Details",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Nazwy nie mogą zawierać znaków sterujących.",
|
||||
"name_too_long": "Ta nazwa jest zbyt długa dla serwera.",
|
||||
"delete": "Usuń",
|
||||
"delete_confirm_title": "Usuń zasób",
|
||||
"delete_confirm_message": "Czy na pewno chcesz usunąć \"{name}\"? Tej operacji nie można cofnąć.",
|
||||
"download": "Pobierz",
|
||||
"name": "Nazwa",
|
||||
"size": "Rozmiar",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Część kopii pozostała na miejscu.",
|
||||
"create_folder_success": "Folder utworzono",
|
||||
"create_folder_error": "Nie udało się utworzyć folderu",
|
||||
"delete_success": "Usunięto pomyślnie",
|
||||
"delete_error": "Nie udało się usunąć",
|
||||
"trash": "Kosz",
|
||||
"restore": "Przywróć",
|
||||
"restored": "{count, plural, one {Przywrócono element} few {Przywrócono # elementy} other {Przywrócono # elementów}}",
|
||||
"restore_error": "Nie udało się przywrócić",
|
||||
"moved_to_trash": "{count, plural, one {Przeniesiono do kosza} few {Przeniesiono # elementy do kosza} other {Przeniesiono # elementów do kosza}}",
|
||||
"delete_forever": "Usuń trwale",
|
||||
"delete_forever_title": "{count, plural, one {Usunąć trwale?} few {Usunąć trwale # elementy?} other {Usunąć trwale # elementów?}}",
|
||||
"delete_forever_message": "„{name}” zostanie usunięty. Tej operacji nie można cofnąć.",
|
||||
"delete_forever_message_count": "{count, plural, one {Zostanie usunięty 1 element} few {Zostaną usunięte # elementy} other {Zostanie usuniętych # elementów}}. Tej operacji nie można cofnąć.",
|
||||
"deleted_forever": "{count, plural, one {Usunięto trwale} few {Usunięto trwale # elementy} other {Usunięto trwale # elementów}}",
|
||||
"empty_trash": "Opróżnij kosz",
|
||||
"empty_trash_title": "Opróżnić kosz?",
|
||||
"empty_trash_message": "{count, plural, one {Element w koszu zostanie usunięty} few {Wszystkie # elementy w koszu zostaną usunięte} other {Wszystkie # elementów w koszu zostanie usuniętych}} trwale. Tej operacji nie można cofnąć.",
|
||||
"trash_emptied": "Kosz opróżniony",
|
||||
"empty_trash_error": "Nie udało się opróżnić kosza",
|
||||
"trash_empty_title": "Kosz jest pusty",
|
||||
"trash_empty_hint": "Usunięte pliki pozostają tutaj, dopóki nie opróżnisz kosza.",
|
||||
"deleted": "Usunięto",
|
||||
"deleted_from": "Usunięto z {folder}",
|
||||
"rename_success": "Nazwę zmieniono pomyślnie",
|
||||
"rename_error": "Nie udało się zmienić nazwy",
|
||||
"download_error": "Nie udało się pobrać",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Utwórz",
|
||||
"save": "Zapisz",
|
||||
"no_results": "Żadne pliki nie pasują do wyszukiwania",
|
||||
"batch_delete_confirm_message": "Czy na pewno chcesz usunąć {count, plural, one {1 element} other {# elementów}}? Tej operacji nie można cofnąć.",
|
||||
"batch_delete_success": "{count, plural, one {Usunięto 1 element} other {Usunięto # elementów}}",
|
||||
"grid_view": "Widok siatki",
|
||||
"list_view": "Widok listy",
|
||||
"details": "Szczegóły",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Os nomes não podem conter caracteres de controle.",
|
||||
"name_too_long": "Este nome é longo demais para o servidor.",
|
||||
"delete": "Eliminar",
|
||||
"delete_confirm_title": "Eliminar recurso",
|
||||
"delete_confirm_message": "Tem a certeza de que deseja eliminar \"{name}\"? Esta ação não pode ser desfeita.",
|
||||
"download": "Transferir",
|
||||
"name": "Nome",
|
||||
"size": "Tamanho",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Parte da cópia ficou no lugar.",
|
||||
"create_folder_success": "Pasta criada",
|
||||
"create_folder_error": "Falha ao criar a pasta",
|
||||
"delete_success": "Eliminado com sucesso",
|
||||
"delete_error": "Falha ao eliminar",
|
||||
"trash": "Lixeira",
|
||||
"restore": "Restaurar",
|
||||
"restored": "{count, plural, one {Item restaurado} other {# itens restaurados}}",
|
||||
"restore_error": "Não foi possível restaurar",
|
||||
"moved_to_trash": "{count, plural, one {Movido para a lixeira} other {# itens movidos para a lixeira}}",
|
||||
"delete_forever": "Excluir permanentemente",
|
||||
"delete_forever_title": "{count, plural, one {Excluir permanentemente?} other {Excluir # itens permanentemente?}}",
|
||||
"delete_forever_message": "\"{name}\" será excluído. Não é possível desfazer.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 item será excluído} other {# itens serão excluídos}}. Não é possível desfazer.",
|
||||
"deleted_forever": "{count, plural, one {Excluído permanentemente} other {# itens excluídos permanentemente}}",
|
||||
"empty_trash": "Esvaziar lixeira",
|
||||
"empty_trash_title": "Esvaziar a lixeira?",
|
||||
"empty_trash_message": "{count, plural, one {O item da lixeira será excluído} other {Todos os # itens da lixeira serão excluídos}} permanentemente. Não é possível desfazer.",
|
||||
"trash_emptied": "Lixeira esvaziada",
|
||||
"empty_trash_error": "Não foi possível esvaziar a lixeira",
|
||||
"trash_empty_title": "A lixeira está vazia",
|
||||
"trash_empty_hint": "Os arquivos excluídos ficam aqui até você esvaziar a lixeira.",
|
||||
"deleted": "Excluído",
|
||||
"deleted_from": "Excluído de {folder}",
|
||||
"rename_success": "Renomeado com sucesso",
|
||||
"rename_error": "Falha ao renomear",
|
||||
"download_error": "Falha ao transferir",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Criar",
|
||||
"save": "Guardar",
|
||||
"no_results": "Nenhum arquivo corresponde à sua pesquisa",
|
||||
"batch_delete_confirm_message": "Tem a certeza de que deseja eliminar {count, plural, one {1 item} other {# itens}}? Esta ação não pode ser desfeita.",
|
||||
"batch_delete_success": "{count, plural, one {1 item eliminado} other {# itens eliminados}}",
|
||||
"grid_view": "Vista em grelha",
|
||||
"list_view": "Vista em lista",
|
||||
"details": "Detalhes",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Numele nu pot conține caractere de control.",
|
||||
"name_too_long": "Acest nume este prea lung pentru server.",
|
||||
"delete": "Șterge",
|
||||
"delete_confirm_title": "Ștergeți resursa",
|
||||
"delete_confirm_message": "Sunteți sigur că doriți să ștergeți „{name}”? Această acțiune nu poate fi anulată.",
|
||||
"download": "Descărcați",
|
||||
"name": "Nume",
|
||||
"size": "Dimensiune",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "O parte din copie a rămas pe loc.",
|
||||
"create_folder_success": "Folder creat",
|
||||
"create_folder_error": "Nu s-a putut crea folderul",
|
||||
"delete_success": "Șters cu succes",
|
||||
"delete_error": "Nu s-a putut șterge",
|
||||
"trash": "Coș de gunoi",
|
||||
"restore": "Restaurați",
|
||||
"restored": "{count, plural, one {Element restaurat} other {# elemente restaurate}}",
|
||||
"restore_error": "Nu s-a putut restaura",
|
||||
"moved_to_trash": "{count, plural, one {Mutat în coșul de gunoi} other {# elemente mutate în coșul de gunoi}}",
|
||||
"delete_forever": "Ștergeți definitiv",
|
||||
"delete_forever_title": "{count, plural, one {Ștergeți definitiv?} other {Ștergeți definitiv # elemente?}}",
|
||||
"delete_forever_message": "„{name}” va fi șters. Acțiunea nu poate fi anulată.",
|
||||
"delete_forever_message_count": "{count, plural, one {Va fi șters 1 element} other {Vor fi șterse # elemente}}. Acțiunea nu poate fi anulată.",
|
||||
"deleted_forever": "{count, plural, one {Șters definitiv} other {# elemente șterse definitiv}}",
|
||||
"empty_trash": "Goliți coșul de gunoi",
|
||||
"empty_trash_title": "Goliți coșul de gunoi?",
|
||||
"empty_trash_message": "{count, plural, one {Elementul din coșul de gunoi va fi șters} other {Toate cele # elemente din coșul de gunoi vor fi șterse}} definitiv. Acțiunea nu poate fi anulată.",
|
||||
"trash_emptied": "Coșul de gunoi a fost golit",
|
||||
"empty_trash_error": "Nu s-a putut goli coșul de gunoi",
|
||||
"trash_empty_title": "Coșul de gunoi este gol",
|
||||
"trash_empty_hint": "Fișierele șterse rămân aici până când goliți coșul de gunoi.",
|
||||
"deleted": "Șters",
|
||||
"deleted_from": "Șters din {folder}",
|
||||
"rename_success": "Redenumit cu succes",
|
||||
"rename_error": "Nu s-a putut redenumi",
|
||||
"download_error": "Descărcarea a eșuat",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Creați",
|
||||
"save": "Salvare",
|
||||
"no_results": "Nu există fișiere care să corespundă căutării dvs.",
|
||||
"batch_delete_confirm_message": "Sunteți sigur că doriți să ștergeți {count, plural, one {1 element} few {# elemente} other {# de elemente}}? Această acțiune nu poate fi anulată.",
|
||||
"batch_delete_success": "{count, plural, one {1 element șters} few {# elemente șterse} other {# de elemente șterse}}",
|
||||
"grid_view": "Vizualizare tabelară",
|
||||
"list_view": "Vizualizare listă",
|
||||
"details": "Detalii",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Имена не могут содержать управляющие символы.",
|
||||
"name_too_long": "Это имя слишком длинное для сервера.",
|
||||
"delete": "Удалить",
|
||||
"delete_confirm_title": "Удалить ресурс",
|
||||
"delete_confirm_message": "Вы уверены, что хотите удалить «{name}»? Это действие нельзя отменить.",
|
||||
"download": "Скачать",
|
||||
"name": "Имя",
|
||||
"size": "Размер",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Часть копии осталась на месте.",
|
||||
"create_folder_success": "Папка создана",
|
||||
"create_folder_error": "Не удалось создать папку",
|
||||
"delete_success": "Успешно удалено",
|
||||
"delete_error": "Не удалось удалить",
|
||||
"trash": "Корзина",
|
||||
"restore": "Восстановить",
|
||||
"restored": "{count, plural, one {Элемент восстановлен} few {Восстановлено # элемента} other {Восстановлено # элементов}}",
|
||||
"restore_error": "Не удалось восстановить",
|
||||
"moved_to_trash": "{count, plural, one {Перемещено в корзину} few {# элемента перемещено в корзину} other {# элементов перемещено в корзину}}",
|
||||
"delete_forever": "Удалить навсегда",
|
||||
"delete_forever_title": "{count, plural, one {Удалить навсегда?} few {Удалить # элемента навсегда?} other {Удалить # элементов навсегда?}}",
|
||||
"delete_forever_message": "«{name}» будет удалён. Это действие нельзя отменить.",
|
||||
"delete_forever_message_count": "{count, plural, one {Будет удалён 1 элемент} few {Будут удалены # элемента} other {Будут удалены # элементов}}. Это действие нельзя отменить.",
|
||||
"deleted_forever": "{count, plural, one {Удалено навсегда} few {# элемента удалено навсегда} other {# элементов удалено навсегда}}",
|
||||
"empty_trash": "Очистить корзину",
|
||||
"empty_trash_title": "Очистить корзину?",
|
||||
"empty_trash_message": "{count, plural, one {Элемент в корзине будет удалён} few {Все # элемента в корзине будут удалены} other {Все # элементов в корзине будут удалены}} навсегда. Это действие нельзя отменить.",
|
||||
"trash_emptied": "Корзина очищена",
|
||||
"empty_trash_error": "Не удалось очистить корзину",
|
||||
"trash_empty_title": "Корзина пуста",
|
||||
"trash_empty_hint": "Удалённые файлы хранятся здесь, пока вы не очистите корзину.",
|
||||
"deleted": "Удалено",
|
||||
"deleted_from": "Удалено из {folder}",
|
||||
"rename_success": "Успешно переименовано",
|
||||
"rename_error": "Не удалось переименовать",
|
||||
"download_error": "Не удалось скачать",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Создать",
|
||||
"save": "Сохранить",
|
||||
"no_results": "Нет файлов, соответствующих вашему запросу",
|
||||
"batch_delete_confirm_message": "Вы уверены, что хотите удалить {count, plural, one {1 элемент} other {# элементов}}? Это действие нельзя отменить.",
|
||||
"batch_delete_success": "{count, plural, one {1 элемент удалён} other {# элементов удалено}}",
|
||||
"grid_view": "Сеткой",
|
||||
"list_view": "Списком",
|
||||
"details": "Подробности",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Názvy nesmú obsahovať riadiace znaky.",
|
||||
"name_too_long": "Tento názov je pre server príliš dlhý.",
|
||||
"delete": "Odstrániť",
|
||||
"delete_confirm_title": "Odstrániť položku",
|
||||
"delete_confirm_message": "Naozaj chcete odstrániť \"{name}\"? Túto akciu nie je možné vrátiť.",
|
||||
"download": "Stiahnuť",
|
||||
"name": "Názov",
|
||||
"size": "Veľkosť",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "Časť kópie zostala na mieste.",
|
||||
"create_folder_success": "Priečinok bol vytvorený",
|
||||
"create_folder_error": "Vytvorenie priečinka zlyhalo",
|
||||
"delete_success": "Úspešne odstránené",
|
||||
"delete_error": "Odstránenie zlyhalo",
|
||||
"trash": "Kôš",
|
||||
"restore": "Obnoviť",
|
||||
"restored": "{count, plural, one {Položka obnovená} few {Obnovené # položky} other {Obnovených # položiek}}",
|
||||
"restore_error": "Obnovenie sa nepodarilo",
|
||||
"moved_to_trash": "{count, plural, one {Presunuté do koša} few {# položky presunuté do koša} other {# položiek presunutých do koša}}",
|
||||
"delete_forever": "Natrvalo odstrániť",
|
||||
"delete_forever_title": "{count, plural, one {Natrvalo odstrániť?} few {Natrvalo odstrániť # položky?} other {Natrvalo odstrániť # položiek?}}",
|
||||
"delete_forever_message": "„{name}“ sa odstráni. Túto akciu nie je možné vrátiť.",
|
||||
"delete_forever_message_count": "{count, plural, one {Odstráni sa 1 položka} few {Odstránia sa # položky} other {Odstráni sa # položiek}}. Túto akciu nie je možné vrátiť.",
|
||||
"deleted_forever": "{count, plural, one {Natrvalo odstránené} few {Natrvalo odstránené # položky} other {Natrvalo odstránených # položiek}}",
|
||||
"empty_trash": "Vyprázdniť kôš",
|
||||
"empty_trash_title": "Vyprázdniť kôš?",
|
||||
"empty_trash_message": "{count, plural, one {Položka v koši sa natrvalo odstráni} few {Všetky # položky v koši sa natrvalo odstránia} other {Všetkých # položiek v koši sa natrvalo odstráni}}. Túto akciu nie je možné vrátiť.",
|
||||
"trash_emptied": "Kôš bol vyprázdnený",
|
||||
"empty_trash_error": "Kôš sa nepodarilo vyprázdniť",
|
||||
"trash_empty_title": "Kôš je prázdny",
|
||||
"trash_empty_hint": "Odstránené súbory tu zostanú, kým kôš nevyprázdnite.",
|
||||
"deleted": "Odstránené",
|
||||
"deleted_from": "Odstránené z {folder}",
|
||||
"rename_success": "Úspešne premenované",
|
||||
"rename_error": "Premenovanie zlyhalo",
|
||||
"download_error": "Sťahovanie zlyhalo",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "Vytvoriť",
|
||||
"save": "Uložiť",
|
||||
"no_results": "Hľadaniu nezodpovedajú žiadne súbory",
|
||||
"batch_delete_confirm_message": "Naozaj chcete odstrániť {count, plural, one {1 položku} other {# položiek}}? Túto akciu nie je možné vrátiť.",
|
||||
"batch_delete_success": "{count, plural, one {Odstránená 1 položka} other {Odstránených # položiek}}",
|
||||
"grid_view": "Zobrazenie mriežky",
|
||||
"list_view": "Zobrazenie zoznamu",
|
||||
"details": "Podrobnosti",
|
||||
|
||||
+19
-5
@@ -1471,8 +1471,6 @@
|
||||
"settings_section": "เริ่มต้นใช้งาน"
|
||||
},
|
||||
"files": {
|
||||
"batch_delete_confirm_message": "คุณแน่ใจหรือไม่ว่าต้องการลบ {count, plural, other {# รายการ}} ที่เลือกไว้? การดำเนินการนี้ไม่สามารถยกเลิกได้",
|
||||
"batch_delete_success": "{count, plural, other {ลบแล้ว # รายการ}}",
|
||||
"breadcrumb_root": "หน้าแรก",
|
||||
"breadcrumbs": "เส้นทางโฟลเดอร์",
|
||||
"cancel": "ยกเลิก",
|
||||
@@ -1491,11 +1489,27 @@
|
||||
"create_folder_success": "สร้างโฟลเดอร์เรียบร้อยแล้ว",
|
||||
"cut": "ตัด",
|
||||
"delete": "ลบ",
|
||||
"delete_confirm_message": "คุณแน่ใจหรือไม่ว่าต้องการลบ “{name}”? การดำเนินการนี้ไม่สามารถยกเลิกได้",
|
||||
"delete_confirm_title": "ลบรายการ",
|
||||
"delete_count": "{count, plural, other {ลบ # รายการ}}",
|
||||
"delete_error": "ลบไม่สำเร็จ",
|
||||
"delete_success": "ลบเรียบร้อยแล้ว",
|
||||
"trash": "ถังขยะ",
|
||||
"restore": "กู้คืน",
|
||||
"restored": "{count, plural, other {กู้คืน # รายการแล้ว}}",
|
||||
"restore_error": "กู้คืนไม่ได้",
|
||||
"moved_to_trash": "{count, plural, other {ย้าย # รายการไปที่ถังขยะแล้ว}}",
|
||||
"delete_forever": "ลบถาวร",
|
||||
"delete_forever_title": "{count, plural, other {ลบ # รายการถาวรหรือไม่?}}",
|
||||
"delete_forever_message": "\"{name}\" จะถูกลบ คุณไม่สามารถเลิกทำได้",
|
||||
"delete_forever_message_count": "{count, plural, other {จะลบ # รายการ}} คุณไม่สามารถเลิกทำได้",
|
||||
"deleted_forever": "{count, plural, other {ลบ # รายการถาวรแล้ว}}",
|
||||
"empty_trash": "ล้างถังขยะ",
|
||||
"empty_trash_title": "ล้างถังขยะหรือไม่?",
|
||||
"empty_trash_message": "{count, plural, other {ทั้ง # รายการในถังขยะจะถูกลบถาวร}} คุณไม่สามารถเลิกทำได้",
|
||||
"trash_emptied": "ล้างถังขยะแล้ว",
|
||||
"empty_trash_error": "ล้างถังขยะไม่ได้",
|
||||
"trash_empty_title": "ถังขยะว่างเปล่า",
|
||||
"trash_empty_hint": "ไฟล์ที่ลบจะอยู่ที่นี่จนกว่าคุณจะล้างถังขยะ",
|
||||
"deleted": "วันที่ลบ",
|
||||
"deleted_from": "ลบจาก {folder}",
|
||||
"details": "รายละเอียด",
|
||||
"details_hint": "เลือกไฟล์หรือโฟลเดอร์เพื่อดูรายละเอียด",
|
||||
"disabled_description": "การอัปโหลดไฟล์ขนาดใหญ่ผ่าน WebDAV อาจทำให้ฐานข้อมูล Stalwart/RocksDB ไม่เสถียร รวมถึงการใช้หน่วยความจำเกินและพื้นที่ดิสก์ ฟังก์ชันนี้ไม่แนะนำให้เปิดใช้งานในสภาพแวดล้อม Production",
|
||||
|
||||
+19
-5
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "Adlar denetim karakterleri içeremez.",
|
||||
"name_too_long": "Bu ad sunucu için çok uzun.",
|
||||
"delete": "Sil",
|
||||
"delete_confirm_title": "Kaynağı sil",
|
||||
"delete_confirm_message": "\"{name}\" silinsin mi? Bu geri alınamaz.",
|
||||
"download": "İndir",
|
||||
"name": "Ad",
|
||||
"size": "Boyut",
|
||||
@@ -3905,8 +3903,26 @@
|
||||
"error_partial_copy": "Kopyanın bir kısmı yerinde kaldı.",
|
||||
"create_folder_success": "Klasör oluşturuldu",
|
||||
"create_folder_error": "Klasör oluşturulamadı",
|
||||
"delete_success": "Başarıyla silindi",
|
||||
"delete_error": "Silinemedi",
|
||||
"trash": "Çöp kutusu",
|
||||
"restore": "Geri yükle",
|
||||
"restored": "{count, plural, one {Öğe geri yüklendi} other {# öğe geri yüklendi}}",
|
||||
"restore_error": "Geri yüklenemedi",
|
||||
"moved_to_trash": "{count, plural, one {Çöp kutusuna taşındı} other {# öğe çöp kutusuna taşındı}}",
|
||||
"delete_forever": "Kalıcı olarak sil",
|
||||
"delete_forever_title": "{count, plural, one {Kalıcı olarak silinsin mi?} other {# öğe kalıcı olarak silinsin mi?}}",
|
||||
"delete_forever_message": "\"{name}\" silinecek. Bu işlem geri alınamaz.",
|
||||
"delete_forever_message_count": "{count, plural, one {1 öğe silinecek} other {# öğe silinecek}}. Bu işlem geri alınamaz.",
|
||||
"deleted_forever": "{count, plural, one {Kalıcı olarak silindi} other {# öğe kalıcı olarak silindi}}",
|
||||
"empty_trash": "Çöp kutusunu boşalt",
|
||||
"empty_trash_title": "Çöp kutusu boşaltılsın mı?",
|
||||
"empty_trash_message": "{count, plural, one {Çöp kutusundaki öğe} other {Çöp kutusundaki # öğenin tümü}} kalıcı olarak silinecek. Bu işlem geri alınamaz.",
|
||||
"trash_emptied": "Çöp kutusu boşaltıldı",
|
||||
"empty_trash_error": "Çöp kutusu boşaltılamadı",
|
||||
"trash_empty_title": "Çöp kutusu boş",
|
||||
"trash_empty_hint": "Silinen dosyalar siz çöp kutusunu boşaltana kadar burada kalır.",
|
||||
"deleted": "Silinme",
|
||||
"deleted_from": "{folder} konumundan silindi",
|
||||
"rename_success": "Başarıyla yeniden adlandırıldı",
|
||||
"rename_error": "Yeniden adlandırılamadı",
|
||||
"download_error": "İndirilemedi",
|
||||
@@ -3915,8 +3931,6 @@
|
||||
"create": "Oluştur",
|
||||
"save": "Kaydet",
|
||||
"no_results": "Aramanızla eşleşen dosya yok",
|
||||
"batch_delete_confirm_message": "{count, plural, one {1 öge} other {# öge}} silinsin mi? Bu geri alınamaz.",
|
||||
"batch_delete_success": "{count, plural, one {1 öge silindi} other {# öge silindi}}",
|
||||
"grid_view": "Izgara görünümü",
|
||||
"list_view": "Liste görünümü",
|
||||
"details": "Ayrıntılar",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "Назви не можуть містити керівні символи.",
|
||||
"name_too_long": "Ця назва задовга для сервера.",
|
||||
"delete": "Видалити",
|
||||
"delete_confirm_title": "Видалити ресурс",
|
||||
"delete_confirm_message": "Ви впевнені, що хочете видалити \"{name}\"? Це неможливо скасувати.",
|
||||
"download": "Завантажити",
|
||||
"name": "Ім'я",
|
||||
"size": "Розмір",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "Частина копії залишилася на місці.",
|
||||
"create_folder_success": "Папка створена",
|
||||
"create_folder_error": "Не вдалося створити папку",
|
||||
"delete_success": "Успішно видалено",
|
||||
"delete_error": "Не вдалося видалити",
|
||||
"trash": "Кошик",
|
||||
"restore": "Відновити",
|
||||
"restored": "{count, plural, one {Елемент відновлено} few {Відновлено # елементи} other {Відновлено # елементів}}",
|
||||
"restore_error": "Не вдалося відновити",
|
||||
"moved_to_trash": "{count, plural, one {Переміщено в кошик} few {# елементи переміщено в кошик} other {# елементів переміщено в кошик}}",
|
||||
"delete_forever": "Видалити назавжди",
|
||||
"delete_forever_title": "{count, plural, one {Видалити назавжди?} few {Видалити # елементи назавжди?} other {Видалити # елементів назавжди?}}",
|
||||
"delete_forever_message": "«{name}» буде видалено. Цю дію не можна скасувати.",
|
||||
"delete_forever_message_count": "{count, plural, one {Буде видалено 1 елемент} few {Буде видалено # елементи} other {Буде видалено # елементів}}. Цю дію не можна скасувати.",
|
||||
"deleted_forever": "{count, plural, one {Видалено назавжди} few {# елементи видалено назавжди} other {# елементів видалено назавжди}}",
|
||||
"empty_trash": "Очистити кошик",
|
||||
"empty_trash_title": "Очистити кошик?",
|
||||
"empty_trash_message": "{count, plural, one {Елемент у кошику буде видалено} few {Усі # елементи в кошику буде видалено} other {Усі # елементів у кошику буде видалено}} назавжди. Цю дію не можна скасувати.",
|
||||
"trash_emptied": "Кошик очищено",
|
||||
"empty_trash_error": "Не вдалося очистити кошик",
|
||||
"trash_empty_title": "Кошик порожній",
|
||||
"trash_empty_hint": "Видалені файли зберігаються тут, доки ви не очистите кошик.",
|
||||
"deleted": "Видалено",
|
||||
"deleted_from": "Видалено з {folder}",
|
||||
"rename_success": "Успішно перейменовано",
|
||||
"rename_error": "Не вдалося перейменувати",
|
||||
"download_error": "Не вдалося завантажити",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "Створити",
|
||||
"save": "зберегти",
|
||||
"no_results": "Немає файлів, що відповідають вашому пошуку",
|
||||
"batch_delete_confirm_message": "Ви впевнені, що хочете видалити {count, plural, one {1 item} other {# items}}? Це неможливо скасувати.",
|
||||
"batch_delete_success": "{count, plural, one {1 елемент видалено} few {# елементи видалено} many {# елементів видалено} other {# елементів видалено}}",
|
||||
"grid_view": "Вигляд сітки",
|
||||
"list_view": "Перегляд списку",
|
||||
"details": "Подробиці",
|
||||
|
||||
@@ -3879,8 +3879,6 @@
|
||||
"name_control_chars": "名稱不能包含控制字元。",
|
||||
"name_too_long": "此名稱對伺服器來說過長。",
|
||||
"delete": "刪除",
|
||||
"delete_confirm_title": "刪除項目",
|
||||
"delete_confirm_message": "您確定要刪除「{name}」嗎?此動作無法復原。",
|
||||
"download": "下載",
|
||||
"name": "名稱",
|
||||
"size": "大小",
|
||||
@@ -3906,8 +3904,26 @@
|
||||
"error_partial_copy": "部分副本仍保留在原處。",
|
||||
"create_folder_success": "資料夾已建立",
|
||||
"create_folder_error": "建立資料夾失敗",
|
||||
"delete_success": "刪除成功",
|
||||
"delete_error": "刪除失敗",
|
||||
"trash": "垃圾桶",
|
||||
"restore": "還原",
|
||||
"restored": "{count, plural, other {已還原 # 個項目}}",
|
||||
"restore_error": "無法還原",
|
||||
"moved_to_trash": "{count, plural, other {已將 # 個項目移到垃圾桶}}",
|
||||
"delete_forever": "永久刪除",
|
||||
"delete_forever_title": "{count, plural, other {要永久刪除 # 個項目嗎?}}",
|
||||
"delete_forever_message": "「{name}」將被刪除。此動作無法復原。",
|
||||
"delete_forever_message_count": "{count, plural, other {將刪除 # 個項目}}。此動作無法復原。",
|
||||
"deleted_forever": "{count, plural, other {已永久刪除 # 個項目}}",
|
||||
"empty_trash": "清空垃圾桶",
|
||||
"empty_trash_title": "要清空垃圾桶嗎?",
|
||||
"empty_trash_message": "{count, plural, other {垃圾桶中的 # 個項目將被永久刪除}}。此動作無法復原。",
|
||||
"trash_emptied": "已清空垃圾桶",
|
||||
"empty_trash_error": "無法清空垃圾桶",
|
||||
"trash_empty_title": "垃圾桶是空的",
|
||||
"trash_empty_hint": "刪除的檔案會保留在這裡,直到您清空垃圾桶。",
|
||||
"deleted": "刪除時間",
|
||||
"deleted_from": "從 {folder} 刪除",
|
||||
"rename_success": "重新命名成功",
|
||||
"rename_error": "重新命名失敗",
|
||||
"download_error": "下載失敗",
|
||||
@@ -3916,8 +3932,6 @@
|
||||
"create": "建立",
|
||||
"save": "儲存",
|
||||
"no_results": "找不到符合搜尋條件的檔案",
|
||||
"batch_delete_confirm_message": "您確定要刪除{count, plural, one {1 個項目} other {# 個項目}}嗎?此動作無法復原。",
|
||||
"batch_delete_success": "{count, plural, one {已刪除 1 個項目} other {已刪除 # 個項目}}",
|
||||
"grid_view": "網格檢視",
|
||||
"list_view": "清單檢視",
|
||||
"details": "詳細資料",
|
||||
|
||||
+19
-5
@@ -3833,8 +3833,6 @@
|
||||
"name_control_chars": "名称不能包含控制字符。",
|
||||
"name_too_long": "此名称对服务器来说过长。",
|
||||
"delete": "删除",
|
||||
"delete_confirm_title": "删除资源",
|
||||
"delete_confirm_message": "您确定要删除\"{name}\"吗?此操作无法撤消。",
|
||||
"download": "下载",
|
||||
"name": "名称",
|
||||
"size": "大小",
|
||||
@@ -3859,8 +3857,26 @@
|
||||
"error_partial_copy": "部分副本被保留在原处。",
|
||||
"create_folder_success": "文件夹已创建",
|
||||
"create_folder_error": "创建文件夹失败",
|
||||
"delete_success": "删除成功",
|
||||
"delete_error": "删除失败",
|
||||
"trash": "回收站",
|
||||
"restore": "恢复",
|
||||
"restored": "{count, plural, other {已恢复 # 个项目}}",
|
||||
"restore_error": "无法恢复",
|
||||
"moved_to_trash": "{count, plural, other {已将 # 个项目移到回收站}}",
|
||||
"delete_forever": "永久删除",
|
||||
"delete_forever_title": "{count, plural, other {永久删除 # 个项目?}}",
|
||||
"delete_forever_message": "“{name}”将被删除。此操作无法撤销。",
|
||||
"delete_forever_message_count": "{count, plural, other {将删除 # 个项目}}。此操作无法撤销。",
|
||||
"deleted_forever": "{count, plural, other {已永久删除 # 个项目}}",
|
||||
"empty_trash": "清空回收站",
|
||||
"empty_trash_title": "清空回收站?",
|
||||
"empty_trash_message": "{count, plural, other {回收站中的 # 个项目将被永久删除}}。此操作无法撤销。",
|
||||
"trash_emptied": "已清空回收站",
|
||||
"empty_trash_error": "无法清空回收站",
|
||||
"trash_empty_title": "回收站为空",
|
||||
"trash_empty_hint": "删除的文件会保留在这里,直到您清空回收站。",
|
||||
"deleted": "删除时间",
|
||||
"deleted_from": "从 {folder} 删除",
|
||||
"rename_success": "重命名成功",
|
||||
"rename_error": "重命名失败",
|
||||
"download_error": "下载失败",
|
||||
@@ -3869,8 +3885,6 @@
|
||||
"create": "创建",
|
||||
"save": "保存",
|
||||
"no_results": "没有与您的搜索匹配的文件",
|
||||
"batch_delete_confirm_message": "确定要删除{count, plural, one {1 个项目} other {# 个项目}}吗?此操作无法撤销。",
|
||||
"batch_delete_success": "{count, plural, one {已删除 1 个项目} other {已删除 # 个项目}}",
|
||||
"grid_view": "网格视图",
|
||||
"list_view": "列表视图",
|
||||
"details": "详情",
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useFileStore, deletesToTrash } from '../file-store';
|
||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||
import type { FileNode } from '@/lib/jmap/types';
|
||||
import { TRASH_LOCATION, parseTrashBatchName, trashBatchName } from '@/lib/files-trash';
|
||||
|
||||
// An in-memory FileNode server: a folder is a node without a blob, and
|
||||
// destroying a node destroys what is below it, as on Stalwart.
|
||||
function makeServer(initial: FileNode[]) {
|
||||
let nodes = initial.map(n => ({ ...n }));
|
||||
let seq = 0;
|
||||
const now = () => new Date().toISOString();
|
||||
const client = {
|
||||
getCapabilities: () => ({}),
|
||||
async listAllFileNodes() { return nodes.map(n => ({ ...n })); },
|
||||
async listAllFileNodesAcrossAccounts() { return nodes.map(n => ({ ...n })); },
|
||||
async createFileDirectory(name: string, parentId: string | null) {
|
||||
const node: FileNode = { id: `n${++seq}`, parentId, name, type: '', blobId: null, size: 0, created: now(), modified: now() };
|
||||
nodes.push(node);
|
||||
return { ...node };
|
||||
},
|
||||
async updateFileNode(id: string, patch: Partial<FileNode>) {
|
||||
Object.assign(nodes.find(n => n.id === id)!, patch);
|
||||
},
|
||||
async updateFileNodes(updates: Record<string, Partial<FileNode>>) {
|
||||
const updated: string[] = [];
|
||||
for (const [id, patch] of Object.entries(updates)) {
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (!node) continue;
|
||||
const clash = nodes.some(n => n.id !== id && (n.parentId ?? null) === (patch.parentId !== undefined ? patch.parentId : node.parentId) && n.name === (patch.name ?? node.name));
|
||||
if (clash) continue;
|
||||
Object.assign(node, patch);
|
||||
updated.push(id);
|
||||
}
|
||||
return { updated, notUpdated: {} };
|
||||
},
|
||||
async destroyFileNodes(ids: string[]) {
|
||||
const gone = new Set(ids);
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const n of nodes) {
|
||||
if (n.parentId && gone.has(n.parentId) && !gone.has(n.id)) { gone.add(n.id); grew = true; }
|
||||
}
|
||||
}
|
||||
nodes = nodes.filter(n => !gone.has(n.id));
|
||||
return { destroyed: [...gone], notDestroyed: [] };
|
||||
},
|
||||
nodes: () => nodes,
|
||||
};
|
||||
return client as unknown as IJMAPClient & { nodes: () => FileNode[] };
|
||||
}
|
||||
|
||||
const dir = (id: string, name: string, parentId: string | null): FileNode => ({
|
||||
id, parentId, name, type: '', blobId: null, size: 0, created: '', modified: '',
|
||||
});
|
||||
const file = (id: string, name: string, parentId: string | null): FileNode => ({
|
||||
id, parentId, name, type: 'text/plain', blobId: `b-${id}`, size: 3, created: '', modified: '',
|
||||
});
|
||||
|
||||
const names = () => useFileStore.getState().resources.map(r => r.name);
|
||||
|
||||
describe('file-store trash', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
useFileStore.setState({
|
||||
client: null, currentParentId: null, currentPath: '/', pathStack: [{ id: null, name: '' }],
|
||||
resources: [], selectedResources: new Set(), clipboard: null, lastAction: null,
|
||||
favorites: [], recentFiles: [], trashCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
async function open(nodes: FileNode[], folder: string | null = null) {
|
||||
const server = makeServer(nodes);
|
||||
useFileStore.getState().initClient(server);
|
||||
await useFileStore.getState().navigate(folder, folder ?? undefined);
|
||||
return server;
|
||||
}
|
||||
|
||||
it('moves deleted items into the trash instead of destroying them, and hides the trash', async () => {
|
||||
const server = await open([dir('docs', 'Documents', null), file('a', 'a.txt', 'docs'), file('b', 'b.txt', 'docs')], 'docs');
|
||||
expect(await useFileStore.getState().trashResources(['a.txt'])).toBe(1);
|
||||
|
||||
expect(server.nodes().find(n => n.id === 'a')).toBeDefined();
|
||||
expect(names()).toEqual(['b.txt']);
|
||||
expect(useFileStore.getState().trashCount).toBe(1);
|
||||
|
||||
await useFileStore.getState().navigate(null);
|
||||
expect(names()).toEqual(['Documents']);
|
||||
expect(useFileStore.getState().allFolders.map(f => f.name)).toEqual(['Documents']);
|
||||
});
|
||||
|
||||
it('records where an item came from in the name of its delete', () => {
|
||||
expect(parseTrashBatchName(trashBatchName('docs', 1730112000000))).toEqual({ deletedAt: 1730112000000, parentId: 'docs' });
|
||||
expect(parseTrashBatchName(trashBatchName(null, 1730112000000))).toEqual({ deletedAt: 1730112000000, parentId: null });
|
||||
expect(parseTrashBatchName('Holiday photos')).toBeNull();
|
||||
});
|
||||
|
||||
it('puts the items back on undo and leaves no empty folder in the trash', async () => {
|
||||
const server = await open([dir('docs', 'Documents', null), file('a', 'a.txt', 'docs')], 'docs');
|
||||
await useFileStore.getState().trashResources(['a.txt']);
|
||||
await useFileStore.getState().undoLastAction();
|
||||
|
||||
expect(names()).toEqual(['a.txt']);
|
||||
expect(useFileStore.getState().trashCount).toBe(0);
|
||||
const trash = server.nodes().find(n => n.name === '.Trash')!;
|
||||
expect(server.nodes().filter(n => n.parentId === trash.id)).toEqual([]);
|
||||
});
|
||||
|
||||
it('lists the trash with where each item came from, and restores it there', async () => {
|
||||
const server = await open([dir('docs', 'Documents', null), file('a', 'a.txt', 'docs')], 'docs');
|
||||
await useFileStore.getState().trashResources(['a.txt']);
|
||||
|
||||
await useFileStore.getState().navigate(TRASH_LOCATION);
|
||||
const [item] = useFileStore.getState().resources;
|
||||
expect(item.name).toBe('a.txt');
|
||||
expect(item.trash?.fromParentId).toBe('docs');
|
||||
expect(useFileStore.getState().currentPath).toBe('/.Trash');
|
||||
|
||||
expect(await useFileStore.getState().restoreResources(['a.txt'])).toBe(1);
|
||||
expect(server.nodes().find(n => n.id === 'a')?.parentId).toBe('docs');
|
||||
expect(names()).toEqual([]);
|
||||
});
|
||||
|
||||
it('restores to the root when the folder is gone, under a new name when the old one is taken', async () => {
|
||||
const server = await open([dir('docs', 'Documents', null), file('a', 'a.txt', 'docs'), file('top', 'a.txt', null)], 'docs');
|
||||
await useFileStore.getState().trashResources(['a.txt']);
|
||||
await server.destroyFileNodes(['docs']);
|
||||
|
||||
await useFileStore.getState().navigate(TRASH_LOCATION);
|
||||
await useFileStore.getState().restoreResources(['a.txt']);
|
||||
const restored = server.nodes().find(n => n.id === 'a')!;
|
||||
expect(restored.parentId).toBeNull();
|
||||
expect(restored.name).toBe('a (1).txt');
|
||||
});
|
||||
|
||||
it('deletes items for good from the trash, and empties it', async () => {
|
||||
const server = await open([file('a', 'a.txt', null), file('b', 'b.txt', null)]);
|
||||
await useFileStore.getState().trashResources(['a.txt']);
|
||||
await useFileStore.getState().trashResources(['b.txt']);
|
||||
await useFileStore.getState().navigate(TRASH_LOCATION);
|
||||
|
||||
expect(await useFileStore.getState().purgeResources(['a.txt'])).toBe(1);
|
||||
expect(server.nodes().find(n => n.id === 'a')).toBeUndefined();
|
||||
expect(names()).toEqual(['b.txt']);
|
||||
|
||||
await useFileStore.getState().emptyTrash();
|
||||
expect(server.nodes()).toEqual([]);
|
||||
expect(useFileStore.getState().trashCount).toBe(0);
|
||||
});
|
||||
|
||||
it('trashes a folder from the tree and steps out of it', async () => {
|
||||
const server = await open([dir('docs', 'Documents', null), dir('sub', 'Sub', 'docs')], 'docs');
|
||||
await useFileStore.getState().navigate('sub', 'Sub');
|
||||
expect(await useFileStore.getState().trashNode('docs')).toBe(true);
|
||||
expect(useFileStore.getState().currentParentId).toBeNull();
|
||||
expect(server.nodes().find(n => n.id === 'docs')).toBeDefined();
|
||||
expect(names()).toEqual([]);
|
||||
});
|
||||
|
||||
it('has no trash in a drive someone shared', () => {
|
||||
expect(deletesToTrash(null)).toBe(true);
|
||||
expect(deletesToTrash('docs')).toBe(true);
|
||||
expect(deletesToTrash('e:team')).toBe(false);
|
||||
expect(deletesToTrash(TRASH_LOCATION)).toBe(false);
|
||||
});
|
||||
});
|
||||
+266
-12
@@ -9,6 +9,10 @@ import { imageBlobUrl } from '@/lib/file-preview';
|
||||
import { fileHooks } from '@/lib/plugin-hooks';
|
||||
import { fileToView } from '@/lib/plugin-projection';
|
||||
import type { FileResourceView } from '@/lib/plugin-types';
|
||||
import {
|
||||
TRASH_FOLDER_NAME, TRASH_LOCATION, TRASH_PATH, findTrashFolder, isTrashLocation, trashBatchName,
|
||||
trashSubtreeIds, trashedItems,
|
||||
} from '@/lib/files-trash';
|
||||
|
||||
export interface FileResource {
|
||||
id: string;
|
||||
@@ -31,6 +35,8 @@ export interface FileResource {
|
||||
isShared?: boolean;
|
||||
ownerAccountId?: string;
|
||||
ownerName?: string;
|
||||
/** Set for an item in the trash: when it was deleted and where from (null: the root). */
|
||||
trash?: { deletedAt: string | null; fromParentId: string | null; batchId: string | null };
|
||||
}
|
||||
|
||||
interface UploadProgress {
|
||||
@@ -73,6 +79,8 @@ interface UndoAction {
|
||||
type: 'rename' | 'move';
|
||||
entries: { id: string; from: Partial<Pick<FileNode, 'name' | 'parentId'>>; to: Partial<Pick<FileNode, 'name' | 'parentId'>> }[];
|
||||
sourceParentId: string | null;
|
||||
/** A delete into the trash: the trash folder its items went into, removed on undo. */
|
||||
trashBatchId?: string;
|
||||
}
|
||||
|
||||
interface FileState {
|
||||
@@ -105,6 +113,8 @@ interface FileState {
|
||||
allFolders: FileResource[];
|
||||
/** False until the first listing for the attached account has finished. */
|
||||
hasListed: boolean;
|
||||
/** How many items the trash holds, from the last listing. */
|
||||
trashCount: number;
|
||||
|
||||
// Actions
|
||||
initClient: (client: IJMAPClient, accountId?: string | null) => void;
|
||||
@@ -160,6 +170,19 @@ interface FileState {
|
||||
renameNode: (id: string, newName: string) => Promise<boolean>;
|
||||
/** Delete any node by id; leaves the folder on screen if it was inside it. */
|
||||
deleteNode: (id: string) => Promise<boolean>;
|
||||
/**
|
||||
* Move items of the folder on screen into the trash; how many went (0 when
|
||||
* a plugin stopped it). Undo puts them back.
|
||||
*/
|
||||
trashResources: (names: string[]) => Promise<number>;
|
||||
/** Move any node by id into the trash, like deleteNode. */
|
||||
trashNode: (id: string) => Promise<boolean>;
|
||||
/** Put items listed in the trash back where they came from (the root when that is gone). */
|
||||
restoreResources: (names: string[]) => Promise<number>;
|
||||
/** Delete items listed in the trash for good. */
|
||||
purgeResources: (names: string[]) => Promise<number>;
|
||||
/** Delete everything in the trash for good. */
|
||||
emptyTrash: () => Promise<void>;
|
||||
/** Download a file by id, wherever it lives. */
|
||||
downloadNode: (id: string) => Promise<void>;
|
||||
removeRecentFile: (id: string) => void;
|
||||
@@ -284,11 +307,45 @@ export function linkedPreviewResource(
|
||||
: resources.find(r => r.name === link.name);
|
||||
}
|
||||
|
||||
// A shared-files page lists files; nothing can be put into it.
|
||||
// A shared-files page lists files; nothing can be put into it, nor into the
|
||||
// trash other than by deleting.
|
||||
function assertCanAddTo(parentId: string | null): void {
|
||||
if (sharedFilesOwner(parentId) !== null) {
|
||||
throw jmapError('Nothing can be added to a list of shared files', { type: 'forbidden' });
|
||||
}
|
||||
if (isTrashLocation(parentId)) {
|
||||
throw jmapError('Nothing can be added to the trash', { type: 'forbidden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether what is deleted in a folder goes to the trash: in the user's own
|
||||
* drive. A drive someone shared has no trash of the user's, and what is
|
||||
* deleted there is gone.
|
||||
*/
|
||||
export function deletesToTrash(parentId: string | null): boolean {
|
||||
return !isCrossAccountId(parentId) && !isTrashLocation(parentId);
|
||||
}
|
||||
|
||||
/** The trash folder, created on the first delete. */
|
||||
async function ensureTrashFolder(client: IJMAPClient, nodes: FileNode[]): Promise<FileNode> {
|
||||
return findTrashFolder(nodes) ?? client.createFileDirectory(TRASH_FOLDER_NAME, null);
|
||||
}
|
||||
|
||||
/** Remove the trash folders of deletes whose items have all left the trash. */
|
||||
async function removeEmptyTrashBatches(client: IJMAPClient, gone: FileResource[]): Promise<void> {
|
||||
const batchIds = [...new Set(gone.map(r => r.trash?.batchId).filter((id): id is string => !!id))];
|
||||
if (batchIds.length === 0) return;
|
||||
const nodes = await client.listAllFileNodes();
|
||||
const empty = batchIds.filter(id => !nodes.some(n => n.parentId === id));
|
||||
if (empty.length > 0) await client.destroyFileNodes(empty);
|
||||
}
|
||||
|
||||
/** Throw the first refusal when nothing of an update went through. */
|
||||
function assertUpdated(result: { updated: string[]; notUpdated: Record<string, string> }): void {
|
||||
if (result.updated.length > 0) return;
|
||||
const [reason] = Object.values(result.notUpdated);
|
||||
throw jmapError(reason || 'The server refused the change', { type: 'forbidden' });
|
||||
}
|
||||
|
||||
// The top of what other accounts share with the user: shared nodes whose
|
||||
@@ -535,6 +592,7 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
sharedRoots: [],
|
||||
allFolders: [],
|
||||
hasListed: false,
|
||||
trashCount: 0,
|
||||
favorites: (() => {
|
||||
try { return JSON.parse(localStorage.getItem('files-favorites') || '[]'); } catch { return []; }
|
||||
})(),
|
||||
@@ -790,6 +848,8 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
} else if (sharedFilesOwner(parentId) !== null) {
|
||||
// A shared-files page sits right below Home, whatever was open before.
|
||||
newStack = [{ id: null, name: '' }, { id: parentId, name: name || parentId }];
|
||||
} else if (isTrashLocation(parentId)) {
|
||||
newStack = [{ id: null, name: '' }, { id: TRASH_LOCATION, name: TRASH_FOLDER_NAME }];
|
||||
} else {
|
||||
// Check if navigating to a parent in the stack
|
||||
const existingIdx = pathStack.findIndex(s => s.id === parentId);
|
||||
@@ -816,25 +876,39 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
? await client.listAllFileNodesAcrossAccounts()
|
||||
: await client.listAllFileNodes();
|
||||
const owner = sharedFilesOwner(parentId);
|
||||
const resources = owner !== null
|
||||
? withUniqueNames(sortResources(
|
||||
sharedRootsOf(allNodes).filter(n => n.accountId === owner && !isFolder(n)).map(nodeToResource),
|
||||
))
|
||||
// The owner's folders on the way down to a shared node aren't shown
|
||||
// (see sharedRootsOf); the user can do nothing in them.
|
||||
: sortResources(childrenOf(allNodes, parentId).filter(n => !n.discoverableOnly).map(nodeToResource));
|
||||
// The trash and what is in it stay out of every listing but its own.
|
||||
const trash = findTrashFolder(allNodes);
|
||||
const inTrash = trashSubtreeIds(allNodes, trash?.id);
|
||||
const trashed = trashedItems(allNodes, trash);
|
||||
const resources = isTrashLocation(parentId)
|
||||
? withUniqueNames(sortResources(trashed.map(item => ({
|
||||
...nodeToResource(item.node),
|
||||
trash: {
|
||||
deletedAt: item.deletedAt !== null ? new Date(item.deletedAt).toISOString() : null,
|
||||
fromParentId: item.fromParentId,
|
||||
batchId: item.batchId,
|
||||
},
|
||||
}))))
|
||||
: owner !== null
|
||||
? withUniqueNames(sortResources(
|
||||
sharedRootsOf(allNodes).filter(n => n.accountId === owner && !isFolder(n)).map(nodeToResource),
|
||||
))
|
||||
// The owner's folders on the way down to a shared node aren't shown
|
||||
// (see sharedRootsOf); the user can do nothing in them.
|
||||
: sortResources(childrenOf(allNodes, parentId).filter(n => !n.discoverableOnly && !inTrash.has(n.id)).map(nodeToResource));
|
||||
// The owner's folders above a shared one stay out of the tree as well.
|
||||
const allFolders = allNodes.filter(n => isFolder(n) && !n.discoverableOnly).map(nodeToResource);
|
||||
const allFolders = allNodes.filter(n => isFolder(n) && !n.discoverableOnly && !inTrash.has(n.id)).map(nodeToResource);
|
||||
|
||||
// Prune recent files whose backing node no longer exists on the server
|
||||
// or is in the trash.
|
||||
const { recentFiles } = get();
|
||||
const existingIds = new Set(allNodes.map(n => n.id));
|
||||
const existingIds = new Set(allNodes.filter(n => !inTrash.has(n.id)).map(n => n.id));
|
||||
const prunedRecent = recentFiles.filter(r => existingIds.has(r.id));
|
||||
if (prunedRecent.length !== recentFiles.length) {
|
||||
try { localStorage.setItem('files-recent-files', JSON.stringify(prunedRecent)); } catch { /* ignore */ }
|
||||
set({ resources, allFolders, recentFiles: prunedRecent, isLoading: false, hasListed: true });
|
||||
set({ resources, allFolders, trashCount: trashed.length, recentFiles: prunedRecent, isLoading: false, hasListed: true });
|
||||
} else {
|
||||
set({ resources, allFolders, isLoading: false, hasListed: true });
|
||||
set({ resources, allFolders, trashCount: trashed.length, isLoading: false, hasListed: true });
|
||||
}
|
||||
if (opensFolder) void fileHooks.onFileNavigate.emit({ path: newPath });
|
||||
} catch (error) {
|
||||
@@ -853,6 +927,10 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
await navigate(null);
|
||||
return;
|
||||
}
|
||||
if (path === TRASH_PATH) {
|
||||
await navigate(TRASH_LOCATION, TRASH_FOLDER_NAME);
|
||||
return;
|
||||
}
|
||||
// Try to match the path against the current pathStack
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const targetDepth = segments.length;
|
||||
@@ -1611,6 +1689,180 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
return true;
|
||||
},
|
||||
|
||||
trashResources: async (names: string[]) => {
|
||||
const { client, resources, currentParentId, currentPath, recentFiles, favorites, refresh } = get();
|
||||
if (!client) return 0;
|
||||
const items = names
|
||||
.map(name => resources.find(r => r.name === name))
|
||||
.filter((r): r is FileResource => !!r);
|
||||
if (items.length === 0) return 0;
|
||||
const files = items.map(r => resourceToView(r, currentPath));
|
||||
if (!(await fileHooks.onBeforeFileDelete.intercept({ files }))) return 0;
|
||||
|
||||
const trash = await ensureTrashFolder(client, await client.listAllFileNodes());
|
||||
const batch = await client.createFileDirectory(trashBatchName(currentParentId), trash.id);
|
||||
let result: { updated: string[]; notUpdated: Record<string, string> };
|
||||
try {
|
||||
result = await client.updateFileNodes(Object.fromEntries(items.map(r => [r.id, { parentId: batch.id }])));
|
||||
assertUpdated(result);
|
||||
} catch (err) {
|
||||
await client.destroyFileNodes([batch.id]).catch(() => undefined);
|
||||
throw err;
|
||||
}
|
||||
const moved = new Set(result.updated);
|
||||
const trashed = items.filter(r => moved.has(r.id));
|
||||
void fileHooks.onAfterFileDelete.emit({ files: files.filter(f => moved.has(f.id)) });
|
||||
|
||||
// Like a delete: recent files and stars of what went drop off.
|
||||
const gonePaths = trashed.filter(r => r.isDirectory).map(r => joinPath(currentPath, r.name));
|
||||
const nextFavorites = favorites.filter(f => !gonePaths.some(path => isUnder(f, path)));
|
||||
const nextRecent = recentFiles.filter(r => !moved.has(r.id));
|
||||
set({
|
||||
favorites: nextFavorites,
|
||||
recentFiles: nextRecent,
|
||||
selectedResources: new Set(),
|
||||
lastAction: {
|
||||
type: 'move',
|
||||
entries: trashed.map(r => ({ id: r.id, from: { parentId: r.parentId ?? null }, to: { parentId: batch.id } })),
|
||||
sourceParentId: currentParentId,
|
||||
trashBatchId: batch.id,
|
||||
},
|
||||
});
|
||||
try {
|
||||
localStorage.setItem('files-favorites', JSON.stringify(nextFavorites));
|
||||
localStorage.setItem('files-recent-files', JSON.stringify(nextRecent));
|
||||
} catch { /* ignore */ }
|
||||
await refresh();
|
||||
return trashed.length;
|
||||
},
|
||||
|
||||
trashNode: async (id: string) => {
|
||||
const { client, allFolders, resources, favorites, recentFiles, pathStack, navigate, refresh } = get();
|
||||
if (!client) return false;
|
||||
const nodes = await client.listAllFileNodes();
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (!node) return false;
|
||||
const path = folderPath(allFolders, id);
|
||||
const known = allFolders.find(f => f.id === id) ?? resources.find(r => r.id === id);
|
||||
const files = [known ? resourceToView(known, parentPath(allFolders, known.parentId)) : unknownNodeView(id)];
|
||||
if (!(await fileHooks.onBeforeFileDelete.intercept({ files }))) return false;
|
||||
|
||||
const trash = await ensureTrashFolder(client, nodes);
|
||||
const batch = await client.createFileDirectory(trashBatchName(node.parentId ?? null), trash.id);
|
||||
try {
|
||||
assertUpdated(await client.updateFileNodes({ [id]: { parentId: batch.id } }));
|
||||
} catch (err) {
|
||||
await client.destroyFileNodes([batch.id]).catch(() => undefined);
|
||||
throw err;
|
||||
}
|
||||
void fileHooks.onAfterFileDelete.emit({ files });
|
||||
|
||||
const nextFavorites = path ? favorites.filter(f => !isUnder(f, path)) : favorites;
|
||||
const nextRecent = recentFiles.filter(r => r.id !== id);
|
||||
set({
|
||||
favorites: nextFavorites,
|
||||
recentFiles: nextRecent,
|
||||
lastAction: {
|
||||
type: 'move',
|
||||
entries: [{ id, from: { parentId: node.parentId ?? null }, to: { parentId: batch.id } }],
|
||||
sourceParentId: node.parentId ?? null,
|
||||
trashBatchId: batch.id,
|
||||
},
|
||||
});
|
||||
try {
|
||||
localStorage.setItem('files-favorites', JSON.stringify(nextFavorites));
|
||||
localStorage.setItem('files-recent-files', JSON.stringify(nextRecent));
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Standing in the deleted folder or below it: step out to its parent.
|
||||
const idx = pathStack.findIndex(s => s.id === id);
|
||||
if (idx > 0) {
|
||||
const parent = pathStack[idx - 1];
|
||||
set({ pathStack: pathStack.slice(0, idx) });
|
||||
await navigate(parent.id, parent.name);
|
||||
return true;
|
||||
}
|
||||
await refresh();
|
||||
return true;
|
||||
},
|
||||
|
||||
restoreResources: async (names: string[]) => {
|
||||
const { client, resources, refresh } = get();
|
||||
if (!client) return 0;
|
||||
const items = names
|
||||
.map(name => resources.find(r => r.name === name && r.trash))
|
||||
.filter((r): r is FileResource => !!r);
|
||||
if (items.length === 0) return 0;
|
||||
|
||||
const nodes = await client.listAllFileNodes();
|
||||
const inTrash = trashSubtreeIds(nodes, findTrashFolder(nodes)?.id);
|
||||
const folderIds = new Set(nodes.filter(n => isFolder(n) && !inTrash.has(n.id)).map(n => n.id));
|
||||
// Names taken in each folder, so a restored item never clashes.
|
||||
const taken = new Map<string | null, Set<string>>();
|
||||
const namesIn = (parentId: string | null) => {
|
||||
let set = taken.get(parentId);
|
||||
if (!set) {
|
||||
set = new Set(childrenOf(nodes, parentId).filter(n => !inTrash.has(n.id)).map(n => n.name));
|
||||
taken.set(parentId, set);
|
||||
}
|
||||
return set;
|
||||
};
|
||||
const updates: Record<string, Partial<Pick<FileNode, 'name' | 'parentId'>>> = {};
|
||||
const targets = new Map<string, string | null>();
|
||||
for (const item of items) {
|
||||
const from = item.trash?.fromParentId ?? null;
|
||||
// A folder that is gone (or itself in the trash) gives the item back to the root.
|
||||
const target = from !== null && folderIds.has(from) ? from : null;
|
||||
const used = namesIn(target);
|
||||
const name = getUniqueName(item.serverName, used);
|
||||
used.add(name);
|
||||
updates[item.id] = name === item.serverName ? { parentId: target } : { parentId: target, name };
|
||||
targets.set(item.id, target);
|
||||
}
|
||||
const result = await client.updateFileNodes(updates);
|
||||
assertUpdated(result);
|
||||
const restored = new Set(result.updated);
|
||||
const allFolders = nodes.filter(n => isFolder(n) && !inTrash.has(n.id)).map(nodeToResource);
|
||||
const byTarget = new Map<string, FileResourceView[]>();
|
||||
for (const item of items.filter(r => restored.has(r.id))) {
|
||||
const toPath = parentPath(allFolders, targets.get(item.id) ?? null);
|
||||
const list = byTarget.get(toPath) ?? [];
|
||||
list.push(resourceToView(item, toPath));
|
||||
byTarget.set(toPath, list);
|
||||
}
|
||||
for (const [toPath, files] of byTarget) void fileHooks.onFileMove.emit({ files, toPath });
|
||||
await removeEmptyTrashBatches(client, items.filter(r => restored.has(r.id)));
|
||||
set({ selectedResources: new Set() });
|
||||
await refresh();
|
||||
return restored.size;
|
||||
},
|
||||
|
||||
purgeResources: async (names: string[]) => {
|
||||
const { client, resources, refresh } = get();
|
||||
if (!client) return 0;
|
||||
const items = names
|
||||
.map(name => resources.find(r => r.name === name && r.trash))
|
||||
.filter((r): r is FileResource => !!r);
|
||||
if (items.length === 0) return 0;
|
||||
// The server removes what is inside a folder with it.
|
||||
const result = await client.destroyFileNodes(items.map(r => r.id));
|
||||
const destroyed = new Set(result.destroyed);
|
||||
await removeEmptyTrashBatches(client, items.filter(r => destroyed.has(r.id)));
|
||||
set({ selectedResources: new Set() });
|
||||
await refresh();
|
||||
return items.filter(r => destroyed.has(r.id)).length;
|
||||
},
|
||||
|
||||
emptyTrash: async () => {
|
||||
const { client, refresh } = get();
|
||||
if (!client) return;
|
||||
const trash = findTrashFolder(await client.listAllFileNodes());
|
||||
// The folder goes with everything in it; the next delete makes a new one.
|
||||
if (trash) await client.destroyFileNodes([trash.id]);
|
||||
set({ selectedResources: new Set() });
|
||||
await refresh();
|
||||
},
|
||||
|
||||
downloadNode: async (id: string) => {
|
||||
const { client } = get();
|
||||
if (!client) return;
|
||||
@@ -1637,6 +1889,8 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
for (const entry of lastAction.entries) {
|
||||
await client.updateFileNode(entry.id, entry.from);
|
||||
}
|
||||
// A delete's folder in the trash is empty again.
|
||||
if (lastAction.trashBatchId) await client.destroyFileNodes([lastAction.trashBatchId]);
|
||||
set({ lastAction: null });
|
||||
void fileHooks.onFileUndo.emit({
|
||||
action: {
|
||||
|
||||
Reference in New Issue
Block a user