refator(ui): merge of ResourceList part1
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Two-slot action-bar layout used above every ResourceList surface.
|
||||||
|
*
|
||||||
|
* [ start-slot ] ← page actions / batch actions
|
||||||
|
* [ end-slot ] ← <DisplayModeControls /> — group-by, sort, view, dotfile
|
||||||
|
*
|
||||||
|
* This is pure layout — no state, no visual variation per section.
|
||||||
|
* It reuses the existing `.actions-bar` / `.action-buttons` classes
|
||||||
|
* defined globally in `styles/ported/content.css` so it renders
|
||||||
|
* identically to `<ListToolbar>` (the component it's replacing).
|
||||||
|
*
|
||||||
|
* Consumers pass whatever they want on either side; `<ResourceList>`
|
||||||
|
* uses this internally to wire its `actions` / `batchActions` /
|
||||||
|
* display-mode-controls snippets, and pages can also use it directly
|
||||||
|
* when they need a bespoke layout that doesn't fit ResourceList's
|
||||||
|
* default (e.g. `/files` upload split-button).
|
||||||
|
*/
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Left cluster — page action buttons (or batch actions on
|
||||||
|
* selection). Omit to render an empty placeholder that still
|
||||||
|
* reserves the space, so the end cluster stays right-aligned. */
|
||||||
|
start?: Snippet;
|
||||||
|
/** Right cluster — usually a `<DisplayModeControls />` instance,
|
||||||
|
* but any content works. */
|
||||||
|
end?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { start, end }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="actions-bar">
|
||||||
|
{#if start}{@render start()}{:else}<div class="action-buttons"></div>{/if}
|
||||||
|
{#if end}{@render end()}{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
/** Reuses the existing GroupOption shape from `<ListToolbar>` so
|
||||||
|
* callers can pass the same `groupBys` arrays their pages already
|
||||||
|
* define. Duplicated here so consumers can import a coherent set
|
||||||
|
* without pulling in the legacy toolbar. */
|
||||||
|
export interface GroupOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Right-hand cluster of display-mode controls: group-by menu,
|
||||||
|
* sort-direction toggle, grid/list view toggle, hide-dotfiles eye.
|
||||||
|
*
|
||||||
|
* Every control is opt-in via its own `show…` prop so a section
|
||||||
|
* without one (e.g. `/trash` has no dotfile toggle by design) can
|
||||||
|
* omit the prop rather than pass an empty array or a no-op
|
||||||
|
* callback. State bindings pass through — the parent still owns
|
||||||
|
* `groupBy`, `reversed`, `viewMode`, etc.
|
||||||
|
*
|
||||||
|
* Style-wise this reuses the ported `.view-toggle` block + child
|
||||||
|
* classes (buttons.css) so it renders identically to the
|
||||||
|
* `<ListToolbar>` right cluster. That keeps every page's look
|
||||||
|
* consistent across the ResourceList migration.
|
||||||
|
*/
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import Icon from '$lib/icons/Icon.svelte';
|
||||||
|
import { t } from '$lib/i18n/index.svelte';
|
||||||
|
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||||
|
import { preferences } from '$lib/stores/preferences.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// ── Group-by ─────────────────────────────────────────────
|
||||||
|
/** Group-by dimensions; omit or empty array = hide the control. */
|
||||||
|
groups?: GroupOption[];
|
||||||
|
/** Active group-by key (controlled by the parent). */
|
||||||
|
groupBy?: string;
|
||||||
|
/** Fired when a group-by dimension is chosen. */
|
||||||
|
ongroup?: (key: string) => void;
|
||||||
|
|
||||||
|
// ── Sort direction ───────────────────────────────────────
|
||||||
|
/** Whether the sort direction is reversed. */
|
||||||
|
reversed?: boolean;
|
||||||
|
/** Fired when the sort-direction toggle is clicked. */
|
||||||
|
ondirection?: () => void;
|
||||||
|
/** Show the sort-direction toggle. Defaults to `true` when
|
||||||
|
* `groups` is non-empty (there's nothing to reverse otherwise). */
|
||||||
|
showSort?: boolean;
|
||||||
|
|
||||||
|
// ── View mode (grid / list) ─────────────────────────────
|
||||||
|
/** Show the grid/list view toggle. */
|
||||||
|
showViewMode?: boolean;
|
||||||
|
|
||||||
|
// ── Dotfile visibility ──────────────────────────────────
|
||||||
|
/** Show the hide-dotfiles eye toggle. Only makes sense on
|
||||||
|
* algorithmic listings (files, recent); off by default. */
|
||||||
|
showDotfileToggle?: boolean;
|
||||||
|
|
||||||
|
// ── Extension slot ──────────────────────────────────────
|
||||||
|
/** Rendered immediately before the group-by button, still
|
||||||
|
* inside `.view-toggle`. Kind-filter dropdowns and other
|
||||||
|
* page-local controls that want to sit alongside the
|
||||||
|
* built-ins land here. */
|
||||||
|
beforeGroupBy?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
groups,
|
||||||
|
groupBy = '',
|
||||||
|
ongroup,
|
||||||
|
reversed = false,
|
||||||
|
ondirection,
|
||||||
|
showSort,
|
||||||
|
showViewMode = false,
|
||||||
|
showDotfileToggle = false,
|
||||||
|
beforeGroupBy
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Sort toggle defaults ON when a group-by list is provided —
|
||||||
|
// there's nothing to reverse without it.
|
||||||
|
const sortVisible = $derived(showSort ?? (groups?.length ?? 0) > 0);
|
||||||
|
|
||||||
|
const active = $derived(groups?.find((g) => g.key === groupBy) ?? groups?.[0]);
|
||||||
|
let menuOpen = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!menuOpen) return;
|
||||||
|
const onDown = (e: MouseEvent) => {
|
||||||
|
if (!(e.target as HTMLElement).closest('.group-by-selector')) menuOpen = false;
|
||||||
|
};
|
||||||
|
window.addEventListener('pointerdown', onDown);
|
||||||
|
return () => window.removeEventListener('pointerdown', onDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
function pick(key: string) {
|
||||||
|
menuOpen = false;
|
||||||
|
ongroup?.(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the whole cluster if nothing is enabled — the ActionBar
|
||||||
|
// then collapses to its start-only layout without an empty
|
||||||
|
// right block occupying space.
|
||||||
|
const anyVisible = $derived(
|
||||||
|
(groups?.length ?? 0) > 0 || showViewMode || showDotfileToggle || !!beforeGroupBy
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if anyVisible}
|
||||||
|
<div class="view-toggle" role="group" aria-label={t('view.label', 'View options')}>
|
||||||
|
{#if beforeGroupBy}{@render beforeGroupBy()}{/if}
|
||||||
|
{#if groups?.length}
|
||||||
|
<div class="group-by-selector" data-testid="display-mode-groupby-menu">
|
||||||
|
<button
|
||||||
|
class="toggle-btn group-by-btn active"
|
||||||
|
title={t('groupby.title', 'Group by')}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
data-testid="display-mode-groupby-btn"
|
||||||
|
onclick={() => (menuOpen = !menuOpen)}
|
||||||
|
>
|
||||||
|
<Icon name={active?.icon ?? 'layer-group'} />
|
||||||
|
<span class="group-by-label">{active?.label ?? ''}</span>
|
||||||
|
</button>
|
||||||
|
{#if sortVisible}
|
||||||
|
<button
|
||||||
|
class="toggle-btn sort-dir-btn"
|
||||||
|
class:active={reversed}
|
||||||
|
title={t('sortdir.title', 'Sort direction')}
|
||||||
|
aria-label={t('sort.direction', 'Sort direction')}
|
||||||
|
data-testid="display-mode-sort-direction-btn"
|
||||||
|
onclick={() => ondirection?.()}
|
||||||
|
>
|
||||||
|
<Icon name="arrow-up" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if menuOpen}
|
||||||
|
<div class="group-by-menu">
|
||||||
|
{#each groups as g (g.key)}
|
||||||
|
<button
|
||||||
|
class="group-by-option"
|
||||||
|
class:active={groupBy === g.key}
|
||||||
|
data-testid={`display-mode-groupby-${g.key}-item`}
|
||||||
|
onclick={() => pick(g.key)}
|
||||||
|
>
|
||||||
|
<Icon name={g.icon ?? 'layer-group'} />
|
||||||
|
{g.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if showViewMode}<span class="view-toggle-separator"></span>{/if}
|
||||||
|
{/if}
|
||||||
|
{#if showViewMode}
|
||||||
|
<button
|
||||||
|
class="toggle-btn"
|
||||||
|
class:active={filesStore.viewMode === 'grid'}
|
||||||
|
title={t('view.grid', 'Grid view')}
|
||||||
|
aria-pressed={filesStore.viewMode === 'grid'}
|
||||||
|
data-testid="display-mode-view-grid-btn"
|
||||||
|
onclick={() => filesStore.setViewMode('grid')}
|
||||||
|
>
|
||||||
|
<Icon name="th" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="toggle-btn"
|
||||||
|
class:active={filesStore.viewMode === 'list'}
|
||||||
|
title={t('view.list', 'List view')}
|
||||||
|
aria-pressed={filesStore.viewMode === 'list'}
|
||||||
|
data-testid="display-mode-view-list-btn"
|
||||||
|
onclick={() => filesStore.setViewMode('list')}
|
||||||
|
>
|
||||||
|
<Icon name="list" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if showDotfileToggle}
|
||||||
|
<button
|
||||||
|
class="toggle-btn"
|
||||||
|
class:active={preferences.hideDotfiles}
|
||||||
|
title={preferences.hideDotfiles
|
||||||
|
? t('view.show_dotfiles', 'Show hidden files')
|
||||||
|
: t('view.hide_dotfiles', 'Hide hidden files')}
|
||||||
|
aria-pressed={preferences.hideDotfiles}
|
||||||
|
data-testid="display-mode-dotfile-toggle-btn"
|
||||||
|
onclick={() => preferences.toggleHideDotfiles()}
|
||||||
|
>
|
||||||
|
<Icon name={preferences.hideDotfiles ? 'eye-slash' : 'eye'} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -69,10 +69,12 @@
|
|||||||
import Icon from '$lib/icons/Icon.svelte';
|
import Icon from '$lib/icons/Icon.svelte';
|
||||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||||
import SkeletonList from '$lib/components/SkeletonList.svelte';
|
import SkeletonList from '$lib/components/SkeletonList.svelte';
|
||||||
import ListToolbar from '$lib/components/ListToolbar.svelte';
|
import ActionBar from '$lib/components/ActionBar.svelte';
|
||||||
|
import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
|
||||||
import UserVignette from '$lib/components/UserVignette.svelte';
|
import UserVignette from '$lib/components/UserVignette.svelte';
|
||||||
import VirtualList from '$lib/components/VirtualList.svelte';
|
import VirtualList from '$lib/components/VirtualList.svelte';
|
||||||
import { t } from '$lib/i18n/index.svelte';
|
import { t } from '$lib/i18n/index.svelte';
|
||||||
|
import { ui } from '$lib/stores/ui.svelte';
|
||||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||||
import { preferences } from '$lib/stores/preferences.svelte';
|
import { preferences } from '$lib/stores/preferences.svelte';
|
||||||
import { formatBytes } from '$lib/utils/format';
|
import { formatBytes } from '$lib/utils/format';
|
||||||
@@ -176,10 +178,62 @@
|
|||||||
onfavorite?: (item: FileItem | FolderItem) => void;
|
onfavorite?: (item: FileItem | FolderItem) => void;
|
||||||
/** Selection changed (set of selected item ids). */
|
/** Selection changed (set of selected item ids). */
|
||||||
onselectionchange?: (ids: Set<string>) => void;
|
onselectionchange?: (ids: Set<string>) => void;
|
||||||
actions?: Snippet<[FileItem | FolderItem]>;
|
/**
|
||||||
toolbar?: Snippet;
|
* Per-item action cell (renders at the end of a row). Kept as a
|
||||||
/** Batch toolbar shown when items are selected; receives selected items. */
|
* distinct slot from the action-bar snippets below so callers
|
||||||
batchToolbar?: Snippet<[Array<FileItem | FolderItem>]>;
|
* that want an item-scoped affordance (a per-row overflow menu)
|
||||||
|
* don't have to piggyback on the bar.
|
||||||
|
*/
|
||||||
|
itemActions?: Snippet<[FileItem | FolderItem]>;
|
||||||
|
/**
|
||||||
|
* Action-bar left cluster — always-visible page action buttons
|
||||||
|
* (Upload / New folder / Empty trash / Clear recent / …). Swaps
|
||||||
|
* to `batchActions` when the selection is non-empty. Every
|
||||||
|
* section provides its own buttons; ResourceList doesn't ship
|
||||||
|
* any defaults.
|
||||||
|
*/
|
||||||
|
actions?: Snippet;
|
||||||
|
/**
|
||||||
|
* Action-bar left cluster when selection is non-empty —
|
||||||
|
* replaces `actions`. Receives the selected items so buttons
|
||||||
|
* can be scoped to the batch. Replaces the phase-1
|
||||||
|
* `batchToolbar` floating strip pattern.
|
||||||
|
*/
|
||||||
|
batchActions?: Snippet<[Array<FileItem | FolderItem>]>;
|
||||||
|
/**
|
||||||
|
* Rendered next to the item name in each row. `/trash` uses
|
||||||
|
* this for its expiration badge; other sections omit it.
|
||||||
|
* ResourceList stays ignorant of what the badge means — the
|
||||||
|
* page decides. Empty return = no badge.
|
||||||
|
*/
|
||||||
|
rowBadge?: Snippet<[FileItem | FolderItem, ItemContext | undefined]>;
|
||||||
|
/**
|
||||||
|
* Rendered above the toolbar in the sticky header. Only
|
||||||
|
* `/files` wires this today; every other section leaves the
|
||||||
|
* snippet undefined so no breadcrumb strip appears. Kept as a
|
||||||
|
* snippet (not a boolean) so the page owns crumb rendering and
|
||||||
|
* their click / drag-drop behavior.
|
||||||
|
*/
|
||||||
|
breadcrumb?: Snippet;
|
||||||
|
/**
|
||||||
|
* When true, drops from the OS file system on the ResourceList
|
||||||
|
* wrapper are forwarded to `onsystemdrop` (upload path). When
|
||||||
|
* false (default), the wrapper still intercepts the OS drop —
|
||||||
|
* `preventDefault` so the browser doesn't navigate to the file
|
||||||
|
* — and fires a "wrong section" `ui.notify()` pointing the user
|
||||||
|
* at the Files section (the legacy behaviour). Item-drag drops
|
||||||
|
* (row → folder) are unaffected either way; those go through
|
||||||
|
* `onitemdrop` per the existing row hooks.
|
||||||
|
*/
|
||||||
|
enableSystemDrop?: boolean;
|
||||||
|
/**
|
||||||
|
* Called with the OS-dropped files when `enableSystemDrop` is
|
||||||
|
* true. The page keeps ownership of the upload code (walking
|
||||||
|
* webkitGetAsEntry trees, chunked uploader, etc.) — this
|
||||||
|
* component just delivers the payload. Ignored when
|
||||||
|
* `enableSystemDrop` is false.
|
||||||
|
*/
|
||||||
|
onsystemdrop?: (e: DragEvent) => void;
|
||||||
/**
|
/**
|
||||||
* Render `<img>` thumbnails on file rows and fall back to
|
* Render `<img>` thumbnails on file rows and fall back to
|
||||||
* client-side generation when the server doesn't have one
|
* client-side generation when the server doesn't have one
|
||||||
@@ -266,9 +320,13 @@
|
|||||||
onopen,
|
onopen,
|
||||||
onfavorite,
|
onfavorite,
|
||||||
onselectionchange,
|
onselectionchange,
|
||||||
|
itemActions,
|
||||||
actions,
|
actions,
|
||||||
toolbar,
|
batchActions,
|
||||||
batchToolbar,
|
rowBadge,
|
||||||
|
breadcrumb,
|
||||||
|
enableSystemDrop = false,
|
||||||
|
onsystemdrop,
|
||||||
enableThumbnails = true,
|
enableThumbnails = true,
|
||||||
isDraggable,
|
isDraggable,
|
||||||
isDropTarget,
|
isDropTarget,
|
||||||
@@ -345,7 +403,7 @@
|
|||||||
showType ? '120px' : '',
|
showType ? '120px' : '',
|
||||||
showSize ? '110px' : '',
|
showSize ? '110px' : '',
|
||||||
showDate ? '160px' : '',
|
showDate ? '160px' : '',
|
||||||
actions ? '120px' : ''
|
itemActions ? '120px' : ''
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
@@ -562,6 +620,63 @@
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── System-drop handling (OS files onto the wrapper) ───────────────────────
|
||||||
|
// Two modes:
|
||||||
|
//
|
||||||
|
// * `enableSystemDrop = true`: the page has an upload code path
|
||||||
|
// ready (the `/files` browser). We `preventDefault` the browser's
|
||||||
|
// default (which would open the dragged file as a top-level
|
||||||
|
// navigation), highlight the drop zone, and hand the DragEvent
|
||||||
|
// off to the page via `onsystemdrop`. The page walks the entries
|
||||||
|
// (webkitGetAsEntry / DataTransferItemList) and drives the upload.
|
||||||
|
//
|
||||||
|
// * `enableSystemDrop = false` (default): the page has no upload
|
||||||
|
// path. Still `preventDefault` so the browser doesn't navigate
|
||||||
|
// away, but instead of forwarding, fire a `ui.notify()` that
|
||||||
|
// points the user at `/files` — this restores the legacy vanilla
|
||||||
|
// frontend's "wrong drop zone" behaviour so users don't wonder
|
||||||
|
// why their drag was silently ignored.
|
||||||
|
//
|
||||||
|
// Row-scoped drops (dragging an in-app row onto a folder row / the
|
||||||
|
// breadcrumb) are handled by the existing `onitemdrop` hooks and use
|
||||||
|
// a private `application/x-oxi-item` MIME so the `Files` type check
|
||||||
|
// below never matches them.
|
||||||
|
let systemDropOver = $state(false);
|
||||||
|
function isSystemDrag(e: DragEvent): boolean {
|
||||||
|
return !!e.dataTransfer?.types?.includes('Files');
|
||||||
|
}
|
||||||
|
function onSystemDragEnter(e: DragEvent) {
|
||||||
|
if (!isSystemDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
systemDropOver = true;
|
||||||
|
}
|
||||||
|
function onSystemDragOver(e: DragEvent) {
|
||||||
|
if (!isSystemDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = enableSystemDrop ? 'copy' : 'none';
|
||||||
|
}
|
||||||
|
function onSystemDragLeave(e: DragEvent) {
|
||||||
|
if (!isSystemDrag(e)) return;
|
||||||
|
systemDropOver = false;
|
||||||
|
}
|
||||||
|
function onSystemDrop(e: DragEvent) {
|
||||||
|
if (!isSystemDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
systemDropOver = false;
|
||||||
|
if (enableSystemDrop && onsystemdrop) {
|
||||||
|
onsystemdrop(e);
|
||||||
|
} else if (!enableSystemDrop) {
|
||||||
|
ui.notify(
|
||||||
|
t(
|
||||||
|
'resource_list.wrong_drop_zone_msg',
|
||||||
|
'Uploads only work in Files — open the Files section and drop there.'
|
||||||
|
),
|
||||||
|
'warning',
|
||||||
|
6000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet row(item: FileItem | FolderItem)}
|
{#snippet row(item: FileItem | FolderItem)}
|
||||||
@@ -644,6 +759,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="name-cell__text">{item.name}</span>
|
<span class="name-cell__text">{item.name}</span>
|
||||||
|
{#if rowBadge}<span class="name-cell__badge">{@render rowBadge(item, ctx)}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if showOwner}
|
{#if showOwner}
|
||||||
<div class="owner-cell">
|
<div class="owner-cell">
|
||||||
@@ -687,51 +803,83 @@
|
|||||||
}}><Icon name={isFav ? 'star' : 'star-outline'} /></button
|
}}><Icon name={isFav ? 'star' : 'star-outline'} /></button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if actions}
|
{#if itemActions}
|
||||||
<div class="action-cell">{@render actions(item)}</div>
|
<div class="action-cell">{@render itemActions(item)}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
`.rl-root` is the drop-target boundary for OS file drops. Every
|
||||||
|
descendant listens through here — dragging in from the desktop over
|
||||||
|
the sticky header or the content area both hit the same handler and
|
||||||
|
either forward to the page's upload code (files browser) or fire the
|
||||||
|
"wrong section" toast (everywhere else).
|
||||||
|
|
||||||
|
svelte-ignore a11y_no_static_element_interactions — the drop handlers
|
||||||
|
are pointer-only fallbacks; keyboard users don't have a "drop a
|
||||||
|
file" action, and the content inside is separately keyboard-nav'd.
|
||||||
|
-->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="rl-root"
|
||||||
|
class:rl-root--drop-over={systemDropOver && enableSystemDrop}
|
||||||
|
ondragenter={onSystemDragEnter}
|
||||||
|
ondragover={onSystemDragOver}
|
||||||
|
ondragleave={onSystemDragLeave}
|
||||||
|
ondrop={onSystemDrop}
|
||||||
|
>
|
||||||
<div class="page-sticky-header">
|
<div class="page-sticky-header">
|
||||||
<h1 class="page-title">{title}</h1>
|
<h1 class="page-title">{title}</h1>
|
||||||
<ListToolbar
|
{#if breadcrumb}
|
||||||
groups={groupBys}
|
<div class="rl-breadcrumb">{@render breadcrumb()}</div>
|
||||||
{groupBy}
|
{/if}
|
||||||
{reversed}
|
<ActionBar>
|
||||||
ongroup={selectGroup}
|
|
||||||
ondirection={toggleDirection}
|
|
||||||
{showViewToggle}
|
|
||||||
{showDotfileToggle}
|
|
||||||
>
|
|
||||||
{#snippet start()}
|
{#snippet start()}
|
||||||
<div class="action-buttons">{@render toolbar?.()}</div>
|
<div class="action-buttons">
|
||||||
|
<!--
|
||||||
|
The action-bar left cluster has two states:
|
||||||
|
1. `batchActions` — when the user has selected items, the
|
||||||
|
page's batch buttons (Move / Delete / Restore / …)
|
||||||
|
replace the default cluster, prefixed with a "clear
|
||||||
|
selection" close button + count label so the batch is
|
||||||
|
dismissable without unchecking every row by hand.
|
||||||
|
2. `actions` — the page's default cluster
|
||||||
|
(Upload / New folder / Empty trash / Clear recent).
|
||||||
|
-->
|
||||||
|
{#if selectable && selected.size > 0 && batchActions}
|
||||||
|
<button
|
||||||
|
class="rl-batch-close"
|
||||||
|
title={t('common.clear', 'Clear selection')}
|
||||||
|
aria-label={t('common.clear', 'Clear selection')}
|
||||||
|
data-testid="resource-list-batch-close-btn"
|
||||||
|
onclick={clearSelection}
|
||||||
|
>
|
||||||
|
<Icon name="times" />
|
||||||
|
</button>
|
||||||
|
<span class="rl-batch-count"
|
||||||
|
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
||||||
|
>
|
||||||
|
{@render batchActions(selectedItems)}
|
||||||
|
{:else if actions}
|
||||||
|
{@render actions()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ListToolbar>
|
{#snippet end()}
|
||||||
|
<DisplayModeControls
|
||||||
|
groups={groupBys}
|
||||||
|
{groupBy}
|
||||||
|
{reversed}
|
||||||
|
ongroup={selectGroup}
|
||||||
|
ondirection={toggleDirection}
|
||||||
|
showViewMode={showViewToggle}
|
||||||
|
{showDotfileToggle}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</ActionBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectable && selected.size > 0 && batchToolbar}
|
|
||||||
<div
|
|
||||||
class="rl-batch"
|
|
||||||
role="region"
|
|
||||||
aria-label={t('files.selection', 'Selection')}
|
|
||||||
data-testid="resource-list-batch-toolbar"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="rl-batch__close"
|
|
||||||
title={t('common.clear', 'Clear')}
|
|
||||||
data-testid="resource-list-batch-close-btn"
|
|
||||||
onclick={clearSelection}
|
|
||||||
>
|
|
||||||
<Icon name="times" />
|
|
||||||
</button>
|
|
||||||
<span class="rl-batch__count"
|
|
||||||
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
|
||||||
>
|
|
||||||
<div class="rl-batch__actions">{@render batchToolbar(selectedItems)}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<EmptyState icon="exclamation-circle" title={error} error />
|
<EmptyState icon="exclamation-circle" title={error} error />
|
||||||
{:else if loading && isEmpty}
|
{:else if loading && isEmpty}
|
||||||
@@ -822,6 +970,7 @@
|
|||||||
<div bind:this={sentinel} class="rl-sentinel" aria-hidden="true"></div>
|
<div bind:this={sentinel} class="rl-sentinel" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div><!-- /.rl-root -->
|
||||||
|
|
||||||
{#snippet listHeader()}
|
{#snippet listHeader()}
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
@@ -842,7 +991,7 @@
|
|||||||
{#if showType}<div>{t('files.col_type', 'Type')}</div>{/if}
|
{#if showType}<div>{t('files.col_type', 'Type')}</div>{/if}
|
||||||
{#if showSize}<div>{t('files.col_size', 'Size')}</div>{/if}
|
{#if showSize}<div>{t('files.col_size', 'Size')}</div>{/if}
|
||||||
{#if showDate}<div>{dateLabel ?? t('files.col_modified', 'Date')}</div>{/if}
|
{#if showDate}<div>{dateLabel ?? t('files.col_modified', 'Date')}</div>{/if}
|
||||||
{#if onfavorite || actions}<div></div>{/if}
|
{#if onfavorite || itemActions}<div></div>{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
@@ -889,19 +1038,47 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Batch toolbar ── */
|
/* ── OS-drop wrapper ──
|
||||||
.rl-batch {
|
`.rl-root` catches drops that miss a specific in-app drop target
|
||||||
display: flex;
|
(row → folder). Its highlight fires ONLY when
|
||||||
align-items: center;
|
`enableSystemDrop && dragging` — the "wrong drop zone" toast path
|
||||||
gap: var(--space-3);
|
deliberately leaves the surface unhighlighted so users don't get a
|
||||||
padding: var(--space-2) var(--space-4);
|
false accept cue. */
|
||||||
margin-bottom: var(--space-3);
|
.rl-root {
|
||||||
background: var(--color-accent-bg);
|
position: relative;
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-batch__close {
|
.rl-root--drop-over::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 2px dashed var(--color-accent);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Breadcrumb strip inside the sticky header ── */
|
||||||
|
.rl-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Row badge (trash expiration, etc.) ── */
|
||||||
|
.name-cell__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Selection controls inside the action bar ──
|
||||||
|
Replaces the deprecated `.rl-batch` floating strip. When items
|
||||||
|
are selected, the close button + count sit before the page's
|
||||||
|
`batchActions` snippet inside `.action-buttons`, so the whole
|
||||||
|
cluster reads as one row of the action bar. */
|
||||||
|
.rl-batch-close {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -914,22 +1091,15 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-batch__close:hover {
|
.rl-batch-close:hover {
|
||||||
background: var(--color-bg-hover);
|
background: var(--color-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-batch__count {
|
.rl-batch-count {
|
||||||
font-weight: var(--weight-semibold);
|
font-weight: var(--weight-semibold);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-batch__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Selection column ── */
|
/* ── Selection column ── */
|
||||||
.select-cell {
|
.select-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
// ── Selection + batch ─────────────────────────────────────────────────────
|
// ── Selection + batch ─────────────────────────────────────────────────────
|
||||||
// Selected items arrive via the batchToolbar snippet param —
|
// Selected items arrive via the batchActions snippet param —
|
||||||
// ResourceList already derives them (O(selection), not O(N)); a
|
// ResourceList already derives them (O(selection), not O(N)); a
|
||||||
// host-side `items.filter(...)` shadow would re-run a second full scan
|
// host-side `items.filter(...)` shadow would re-run a second full scan
|
||||||
// per selection toggle, and its id mirror is unnecessary (the component
|
// per selection toggle, and its id mirror is unnecessary (the component
|
||||||
@@ -307,6 +307,7 @@
|
|||||||
onopen={open}
|
onopen={open}
|
||||||
onfavorite={unfavorite}
|
onfavorite={unfavorite}
|
||||||
showOwner
|
showOwner
|
||||||
|
showPath
|
||||||
selectable
|
selectable
|
||||||
{contextActions}
|
{contextActions}
|
||||||
{groupBys}
|
{groupBys}
|
||||||
@@ -317,7 +318,7 @@
|
|||||||
load(true, orderBy, rev);
|
load(true, orderBy, rev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#snippet batchToolbar(sel)}
|
{#snippet batchActions(sel)}
|
||||||
<Button
|
<Button
|
||||||
icon="download"
|
icon="download"
|
||||||
data-testid="favorites-batch-download-btn"
|
data-testid="favorites-batch-download-btn"
|
||||||
|
|||||||
@@ -292,7 +292,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
// ── Selection + batch ─────────────────────────────────────────────────────
|
// ── Selection + batch ─────────────────────────────────────────────────────
|
||||||
// Selected items arrive via the batchToolbar snippet param —
|
// Selected items arrive via the batchActions snippet param —
|
||||||
// ResourceList already derives them (O(selection), not O(N)); a
|
// ResourceList already derives them (O(selection), not O(N)); a
|
||||||
// host-side `items.filter(...)` shadow would re-run a second full scan
|
// host-side `items.filter(...)` shadow would re-run a second full scan
|
||||||
// per selection toggle, and its id mirror is unnecessary (the component
|
// per selection toggle, and its id mirror is unnecessary (the component
|
||||||
@@ -356,6 +356,7 @@
|
|||||||
onopen={open}
|
onopen={open}
|
||||||
onfavorite={toggleFavorite}
|
onfavorite={toggleFavorite}
|
||||||
showOwner
|
showOwner
|
||||||
|
showPath
|
||||||
showDotfileToggle
|
showDotfileToggle
|
||||||
selectable
|
selectable
|
||||||
{contextActions}
|
{contextActions}
|
||||||
@@ -367,14 +368,14 @@
|
|||||||
load(true, orderBy, rev);
|
load(true, orderBy, rev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#snippet toolbar()}
|
{#snippet actions()}
|
||||||
{#if items.length > 0}
|
{#if items.length > 0}
|
||||||
<Button icon="broom" data-testid="recent-clear-btn" onclick={clearAll}
|
<Button icon="broom" data-testid="recent-clear-btn" onclick={clearAll}
|
||||||
>{t('recent.clear', 'Clear recent')}</Button
|
>{t('recent.clear', 'Clear recent')}</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet batchToolbar(sel)}
|
{#snippet batchActions(sel)}
|
||||||
<Button
|
<Button
|
||||||
icon="download"
|
icon="download"
|
||||||
data-testid="recent-batch-download-btn"
|
data-testid="recent-batch-download-btn"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
restoreTrashItem
|
restoreTrashItem
|
||||||
} from '$lib/api/endpoints/trash';
|
} from '$lib/api/endpoints/trash';
|
||||||
import { dateBucket, sizeBucket, typeLabel } from '$lib/api/endpoints/favorites';
|
import { dateBucket, sizeBucket, typeLabel } from '$lib/api/endpoints/favorites';
|
||||||
|
import { formatDate } from '$lib/utils/display';
|
||||||
import type { Drive, FileItem, FolderItem, TrashResourceItem } from '$lib/api/types';
|
import type { Drive, FileItem, FolderItem, TrashResourceItem } from '$lib/api/types';
|
||||||
import Icon from '$lib/icons/Icon.svelte';
|
import Icon from '$lib/icons/Icon.svelte';
|
||||||
import ResourceList, {
|
import ResourceList, {
|
||||||
@@ -268,8 +269,10 @@
|
|||||||
emptyText={t('trash.empty_state', 'Trash is empty')}
|
emptyText={t('trash.empty_state', 'Trash is empty')}
|
||||||
hasMore={!!cursor}
|
hasMore={!!cursor}
|
||||||
onloadmore={() => load(false, orderByForGroup())}
|
onloadmore={() => load(false, orderByForGroup())}
|
||||||
|
selectable
|
||||||
|
showPath
|
||||||
pathLabel={t('trash.original_location', 'Original location')}
|
pathLabel={t('trash.original_location', 'Original location')}
|
||||||
dateLabel={t('trash.remaining', 'Remaining')}
|
dateLabel={t('trash.expires_at', 'Expires at')}
|
||||||
{groupBys}
|
{groupBys}
|
||||||
bind:groupBy
|
bind:groupBy
|
||||||
bind:reversed
|
bind:reversed
|
||||||
@@ -278,7 +281,7 @@
|
|||||||
load(true, orderBy, rev);
|
load(true, orderBy, rev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#snippet toolbar()}
|
{#snippet actions()}
|
||||||
{#if items.length > 0}
|
{#if items.length > 0}
|
||||||
<button class="btn btn-danger" data-testid="trash-empty-btn" onclick={purgeAll}>
|
<button class="btn btn-danger" data-testid="trash-empty-btn" onclick={purgeAll}>
|
||||||
<Icon name="trash" />
|
<Icon name="trash" />
|
||||||
@@ -286,13 +289,36 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet dateCell(_item, ctx)}
|
{#snippet batchActions(sel)}
|
||||||
|
<button
|
||||||
|
class="btn-action"
|
||||||
|
data-testid="trash-batch-restore-btn"
|
||||||
|
title={t('trash.restore', 'Restore')}
|
||||||
|
onclick={() => sel.forEach(restore)}
|
||||||
|
>
|
||||||
|
<Icon name="undo" />
|
||||||
|
{t('trash.restore', 'Restore')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-action btn-action--delete"
|
||||||
|
data-testid="trash-batch-delete-btn"
|
||||||
|
title={t('trash.delete', 'Delete permanently')}
|
||||||
|
onclick={() => sel.forEach(purge)}
|
||||||
|
>
|
||||||
|
<Icon name="trash" />
|
||||||
|
{t('trash.delete', 'Delete permanently')}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet rowBadge(_item, ctx)}
|
||||||
{@const chip = expiryChip(ctx?.date)}
|
{@const chip = expiryChip(ctx?.date)}
|
||||||
<span class="expiry-chip expiry-chip--{chip.tier}">
|
<span class="expiry-chip expiry-chip--{chip.tier}">
|
||||||
<Icon name={chip.icon} class="expiry-chip__icon" />
|
<Icon name={chip.icon} class="expiry-chip__icon" />
|
||||||
{chip.label}
|
{chip.label}
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet dateCell(_item, ctx)}
|
||||||
|
{formatDate(ctx?.date)}
|
||||||
|
{/snippet}
|
||||||
{#snippet bucketAction(bucketKey: string)}
|
{#snippet bucketAction(bucketKey: string)}
|
||||||
{#if showPerDriveEmpty}
|
{#if showPerDriveEmpty}
|
||||||
{@const driveId = driveIdFromBucketKey(bucketKey)}
|
{@const driveId = driveIdFromBucketKey(bucketKey)}
|
||||||
@@ -310,7 +336,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet actions(item)}
|
{#snippet itemActions(item)}
|
||||||
<button
|
<button
|
||||||
class="btn-action"
|
class="btn-action"
|
||||||
data-testid={`trash-restore-btn-${item.id}`}
|
data-testid={`trash-restore-btn-${item.id}`}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "وقت الحذف"
|
"trashed_time": "وقت الحذف"
|
||||||
},
|
},
|
||||||
"delete": "حذف نهائياً",
|
"delete": "حذف نهائياً",
|
||||||
"empty_action": "تفريغ سلة المهملات"
|
"empty_action": "تفريغ سلة المهملات",
|
||||||
|
"expires_at": "ينتهي في"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "منتهية الصلاحية",
|
"expired": "منتهية الصلاحية",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "لا يوجد أعضاء",
|
"members_empty": "لا يوجد أعضاء",
|
||||||
"member_count": "{{n}} أعضاء"
|
"member_count": "{{n}} أعضاء"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "الموقع",
|
||||||
|
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Löschzeit"
|
"trashed_time": "Löschzeit"
|
||||||
},
|
},
|
||||||
"delete": "Endgültig löschen",
|
"delete": "Endgültig löschen",
|
||||||
"empty_action": "Papierkorb leeren"
|
"empty_action": "Papierkorb leeren",
|
||||||
|
"expires_at": "Läuft ab"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Abgelaufen",
|
"expired": "Abgelaufen",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Keine Mitglieder",
|
"members_empty": "Keine Mitglieder",
|
||||||
"member_count": "{{n}} Mitglieder"
|
"member_count": "{{n}} Mitglieder"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Speicherort",
|
||||||
|
"wrong_drop_zone_msg": "Uploads funktionieren nur in Dateien — öffne den Bereich Dateien und lege die Elemente dort ab."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,7 +618,8 @@
|
|||||||
"confirm_empty": "Empty the trash? This cannot be undone.",
|
"confirm_empty": "Empty the trash? This cannot be undone.",
|
||||||
"delete": "Delete permanently",
|
"delete": "Delete permanently",
|
||||||
"empty_action": "Empty trash",
|
"empty_action": "Empty trash",
|
||||||
"restored": "Restored"
|
"restored": "Restored",
|
||||||
|
"expires_at": "Expires at"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
@@ -1663,5 +1664,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "No members",
|
"members_empty": "No members",
|
||||||
"member_count": "{{n}} members"
|
"member_count": "{{n}} members"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Location",
|
||||||
|
"wrong_drop_zone_msg": "Uploads only work in Files — open the Files section and drop there."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,7 +466,8 @@
|
|||||||
"trashed_time": "Fecha de eliminación"
|
"trashed_time": "Fecha de eliminación"
|
||||||
},
|
},
|
||||||
"delete": "Eliminar permanentemente",
|
"delete": "Eliminar permanentemente",
|
||||||
"empty_action": "Vaciar papelera"
|
"empty_action": "Vaciar papelera",
|
||||||
|
"expires_at": "Expira"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Caducado",
|
"expired": "Caducado",
|
||||||
@@ -1280,5 +1281,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Sin miembros",
|
"members_empty": "Sin miembros",
|
||||||
"member_count": "{{n}} miembros"
|
"member_count": "{{n}} miembros"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Ubicación",
|
||||||
|
"wrong_drop_zone_msg": "Las subidas solo funcionan en Archivos — abre la sección Archivos y suelta ahí los elementos."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "زمان حذف"
|
"trashed_time": "زمان حذف"
|
||||||
},
|
},
|
||||||
"delete": "حذف دائمی",
|
"delete": "حذف دائمی",
|
||||||
"empty_action": "Empty trash"
|
"empty_action": "Empty trash",
|
||||||
|
"expires_at": "انقضا در"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "منقضی شده",
|
"expired": "منقضی شده",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "بدون عضو",
|
"members_empty": "بدون عضو",
|
||||||
"member_count": "{{n}} عضو"
|
"member_count": "{{n}} عضو"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "مکان",
|
||||||
|
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پروندهها کار میکند — بخش پروندهها را باز کنید و آنجا رها کنید."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Date de suppression"
|
"trashed_time": "Date de suppression"
|
||||||
},
|
},
|
||||||
"delete": "Supprimer définitivement",
|
"delete": "Supprimer définitivement",
|
||||||
"empty_action": "Vider la corbeille"
|
"empty_action": "Vider la corbeille",
|
||||||
|
"expires_at": "Expiration"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Expiré",
|
"expired": "Expiré",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Aucun membre",
|
"members_empty": "Aucun membre",
|
||||||
"member_count": "{{n}} membres"
|
"member_count": "{{n}} membres"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Emplacement",
|
||||||
|
"wrong_drop_zone_msg": "Les envois ne fonctionnent que dans Fichiers — ouvrez la section Fichiers et déposez-y vos éléments."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "हटाने का समय"
|
"trashed_time": "हटाने का समय"
|
||||||
},
|
},
|
||||||
"delete": "स्थायी रूप से हटाएँ",
|
"delete": "स्थायी रूप से हटाएँ",
|
||||||
"empty_action": "रद्दी खाली करें"
|
"empty_action": "रद्दी खाली करें",
|
||||||
|
"expires_at": "समाप्ति"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "समाप्त",
|
"expired": "समाप्त",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "कोई सदस्य नहीं",
|
"members_empty": "कोई सदस्य नहीं",
|
||||||
"member_count": "{{n}} सदस्य"
|
"member_count": "{{n}} सदस्य"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "स्थान",
|
||||||
|
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Data di eliminazione"
|
"trashed_time": "Data di eliminazione"
|
||||||
},
|
},
|
||||||
"delete": "Elimina definitivamente",
|
"delete": "Elimina definitivamente",
|
||||||
"empty_action": "Svuota il cestino"
|
"empty_action": "Svuota il cestino",
|
||||||
|
"expires_at": "Scadenza"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Scaduto",
|
"expired": "Scaduto",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Nessun membro",
|
"members_empty": "Nessun membro",
|
||||||
"member_count": "{{n}} membri"
|
"member_count": "{{n}} membri"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Posizione",
|
||||||
|
"wrong_drop_zone_msg": "I caricamenti funzionano solo in File — apri la sezione File e trascina lì gli elementi."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "削除日時"
|
"trashed_time": "削除日時"
|
||||||
},
|
},
|
||||||
"delete": "完全に削除",
|
"delete": "完全に削除",
|
||||||
"empty_action": "ゴミ箱を空にする"
|
"empty_action": "ゴミ箱を空にする",
|
||||||
|
"expires_at": "期限"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "期限切れ",
|
"expired": "期限切れ",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "メンバーなし",
|
"members_empty": "メンバーなし",
|
||||||
"member_count": "{{n}} 人のメンバー"
|
"member_count": "{{n}} 人のメンバー"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "場所",
|
||||||
|
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,7 +583,8 @@
|
|||||||
"empty_action": "휴지통 비우기",
|
"empty_action": "휴지통 비우기",
|
||||||
"confirm_delete": "이 항목을 영구적으로 삭제하시겠습니까? 되돌릴 수 없습니다.",
|
"confirm_delete": "이 항목을 영구적으로 삭제하시겠습니까? 되돌릴 수 없습니다.",
|
||||||
"confirm_empty": "휴지통을 비우시겠습니까? 되돌릴 수 없습니다.",
|
"confirm_empty": "휴지통을 비우시겠습니까? 되돌릴 수 없습니다.",
|
||||||
"restored": "복원됨"
|
"restored": "복원됨",
|
||||||
|
"expires_at": "만료일"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "만료됨",
|
"expired": "만료됨",
|
||||||
@@ -1659,5 +1660,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "구성원 없음",
|
"members_empty": "구성원 없음",
|
||||||
"member_count": "구성원 {{n}}명"
|
"member_count": "구성원 {{n}}명"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "위치",
|
||||||
|
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Verwijderd op"
|
"trashed_time": "Verwijderd op"
|
||||||
},
|
},
|
||||||
"delete": "Permanent verwijderen",
|
"delete": "Permanent verwijderen",
|
||||||
"empty_action": "Prullenbak legen"
|
"empty_action": "Prullenbak legen",
|
||||||
|
"expires_at": "Verloopt op"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Verlopen",
|
"expired": "Verlopen",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Geen leden",
|
"members_empty": "Geen leden",
|
||||||
"member_count": "{{n}} leden"
|
"member_count": "{{n}} leden"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Locatie",
|
||||||
|
"wrong_drop_zone_msg": "Uploaden werkt alleen in Bestanden — open het onderdeel Bestanden en zet ze daar neer."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Czas usunięcia"
|
"trashed_time": "Czas usunięcia"
|
||||||
},
|
},
|
||||||
"delete": "Usuń trwale",
|
"delete": "Usuń trwale",
|
||||||
"empty_action": "Opróżnij kosz"
|
"empty_action": "Opróżnij kosz",
|
||||||
|
"expires_at": "Wygasa"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Wygasł",
|
"expired": "Wygasł",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Brak członków",
|
"members_empty": "Brak członków",
|
||||||
"member_count": "{{n}} członków"
|
"member_count": "{{n}} członków"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Lokalizacja",
|
||||||
|
"wrong_drop_zone_msg": "Przesyłanie działa tylko w Plikach — otwórz sekcję Pliki i upuść tam pliki."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Data de exclusão"
|
"trashed_time": "Data de exclusão"
|
||||||
},
|
},
|
||||||
"delete": "Excluir permanentemente",
|
"delete": "Excluir permanentemente",
|
||||||
"empty_action": "Esvaziar lixeira"
|
"empty_action": "Esvaziar lixeira",
|
||||||
|
"expires_at": "Expira em"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Expirado",
|
"expired": "Expirado",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Sem membros",
|
"members_empty": "Sem membros",
|
||||||
"member_count": "{{n}} membros"
|
"member_count": "{{n}} membros"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Localização",
|
||||||
|
"wrong_drop_zone_msg": "Os envios só funcionam em Ficheiros — abra a secção Ficheiros e largue-os aí."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "Время удаления"
|
"trashed_time": "Время удаления"
|
||||||
},
|
},
|
||||||
"delete": "Удалить навсегда",
|
"delete": "Удалить навсегда",
|
||||||
"empty_action": "Очистить корзину"
|
"empty_action": "Очистить корзину",
|
||||||
|
"expires_at": "Истекает"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "Истёк",
|
"expired": "Истёк",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "Нет участников",
|
"members_empty": "Нет участников",
|
||||||
"member_count": "{{n}} участников"
|
"member_count": "{{n}} участников"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "Расположение",
|
||||||
|
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "刪除時間"
|
"trashed_time": "刪除時間"
|
||||||
},
|
},
|
||||||
"delete": "永久刪除",
|
"delete": "永久刪除",
|
||||||
"empty_action": "清空回收站"
|
"empty_action": "清空回收站",
|
||||||
|
"expires_at": "到期時間"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "已過期",
|
"expired": "已過期",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "無成員",
|
"members_empty": "無成員",
|
||||||
"member_count": "{{n}} 位成員"
|
"member_count": "{{n}} 位成員"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "位置",
|
||||||
|
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,8 @@
|
|||||||
"trashed_time": "删除时间"
|
"trashed_time": "删除时间"
|
||||||
},
|
},
|
||||||
"delete": "永久删除",
|
"delete": "永久删除",
|
||||||
"empty_action": "Empty trash"
|
"empty_action": "Empty trash",
|
||||||
|
"expires_at": "到期时间"
|
||||||
},
|
},
|
||||||
"daysRemaining": {
|
"daysRemaining": {
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
@@ -1265,5 +1266,9 @@
|
|||||||
"group": {
|
"group": {
|
||||||
"members_empty": "无成员",
|
"members_empty": "无成员",
|
||||||
"member_count": "{{n}} 名成员"
|
"member_count": "{{n}} 名成员"
|
||||||
|
},
|
||||||
|
"resource_list": {
|
||||||
|
"location": "位置",
|
||||||
|
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user