refactor(front): apply formatter
This commit is contained in:
@@ -695,7 +695,12 @@
|
|||||||
const bandBottom = bandTop + rubberband.h;
|
const bandBottom = bandTop + rubberband.h;
|
||||||
|
|
||||||
const rows = rlRoot.querySelectorAll<HTMLElement>('.file-item[data-item-id]');
|
const rows = rlRoot.querySelectorAll<HTMLElement>('.file-item[data-item-id]');
|
||||||
const nextSelection = new Set(rubberband.baseline);
|
// Transient scratch set for computing the diff before mutating
|
||||||
|
// `selected`. `SvelteSet` (not plain `Set`) per the codebase's
|
||||||
|
// `svelte/prefer-svelte-reactivity` convention — the lint rule
|
||||||
|
// exists so a future refactor that stashes this in `$state`
|
||||||
|
// can't silently break reactivity.
|
||||||
|
const nextSelection = new SvelteSet(rubberband.baseline);
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const id = row.dataset.itemId;
|
const id = row.dataset.itemId;
|
||||||
if (!id) continue;
|
if (!id) continue;
|
||||||
@@ -1082,14 +1087,14 @@
|
|||||||
ondragleave={onSystemDragLeave}
|
ondragleave={onSystemDragLeave}
|
||||||
ondrop={onSystemDrop}
|
ondrop={onSystemDrop}
|
||||||
>
|
>
|
||||||
<div class="page-sticky-header">
|
<div class="page-sticky-header">
|
||||||
<h1 class="page-title">{title}</h1>
|
<h1 class="page-title">{title}</h1>
|
||||||
{#if breadcrumb}
|
{#if breadcrumb}
|
||||||
<div class="rl-breadcrumb">{@render breadcrumb()}</div>
|
<div class="rl-breadcrumb">{@render breadcrumb()}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<ActionBar>
|
<ActionBar>
|
||||||
{#snippet start()}
|
{#snippet start()}
|
||||||
<!--
|
<!--
|
||||||
The action-bar left cluster has two states:
|
The action-bar left cluster has two states:
|
||||||
1. `batchActions` — when the user has selected items, the
|
1. `batchActions` — when the user has selected items, the
|
||||||
page's batch buttons (Move / Delete / Restore / …)
|
page's batch buttons (Move / Delete / Restore / …)
|
||||||
@@ -1103,149 +1108,150 @@
|
|||||||
2. `actions` — the page's default cluster
|
2. `actions` — the page's default cluster
|
||||||
(Upload / New folder / Empty trash / Clear recent).
|
(Upload / New folder / Empty trash / Clear recent).
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class="action-buttons"
|
class="action-buttons"
|
||||||
class:batch-selection-bar={selectable && selected.size > 0 && batchActions}
|
class:batch-selection-bar={selectable && selected.size > 0 && batchActions}
|
||||||
>
|
>
|
||||||
{#if selectable && selected.size > 0 && batchActions}
|
{#if selectable && selected.size > 0 && batchActions}
|
||||||
<button
|
<button
|
||||||
class="batch-bar-close"
|
class="batch-bar-close"
|
||||||
title={t('common.clear', 'Clear selection')}
|
title={t('common.clear', 'Clear selection')}
|
||||||
aria-label={t('common.clear', 'Clear selection')}
|
aria-label={t('common.clear', 'Clear selection')}
|
||||||
data-testid="resource-list-batch-close-btn"
|
data-testid="resource-list-batch-close-btn"
|
||||||
onclick={clearSelection}
|
onclick={clearSelection}
|
||||||
>
|
>
|
||||||
<Icon name="times" />
|
<Icon name="times" />
|
||||||
</button>
|
</button>
|
||||||
<span class="batch-bar-count"
|
<span class="batch-bar-count"
|
||||||
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
||||||
>
|
>
|
||||||
<div class="batch-bar-actions">
|
<div class="batch-bar-actions">
|
||||||
{@render batchActions(selectedItems)}
|
{@render batchActions(selectedItems)}
|
||||||
</div>
|
</div>
|
||||||
{:else if actions}
|
{:else if actions}
|
||||||
{@render actions()}
|
{@render actions()}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet end()}
|
{#snippet end()}
|
||||||
<DisplayModeControls
|
<DisplayModeControls
|
||||||
groups={groupBys}
|
groups={groupBys}
|
||||||
{groupBy}
|
{groupBy}
|
||||||
{reversed}
|
{reversed}
|
||||||
ongroup={selectGroup}
|
ongroup={selectGroup}
|
||||||
ondirection={toggleDirection}
|
ondirection={toggleDirection}
|
||||||
showViewMode={showViewToggle}
|
showViewMode={showViewToggle}
|
||||||
{showDotfileToggle}
|
{showDotfileToggle}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ActionBar>
|
</ActionBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#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}
|
||||||
<SkeletonList count={SKELETON.length} />
|
<SkeletonList count={SKELETON.length} />
|
||||||
{:else if isEmpty}
|
{:else if isEmpty}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={emptyIcon}
|
icon={emptyIcon}
|
||||||
title={emptyText ?? t('common.empty', 'Nothing here yet.')}
|
title={emptyText ?? t('common.empty', 'Nothing here yet.')}
|
||||||
hint={emptyHint}
|
hint={emptyHint}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="files-container" bind:clientWidth={gridWidth}>
|
<div class="files-container" bind:clientWidth={gridWidth}>
|
||||||
{#if grouped && filesStore.viewMode === 'list'}
|
{#if grouped && filesStore.viewMode === 'list'}
|
||||||
<div class="files-list-view" style="--files-list-columns: {columns}">
|
<div class="files-list-view" style="--files-list-columns: {columns}">
|
||||||
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if}
|
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if}
|
||||||
{#each sections as section (section.key)}
|
{#each sections as section (section.key)}
|
||||||
<div class="rl-swimlane-header" role="rowheader">
|
<div class="rl-swimlane-header" role="rowheader">
|
||||||
<span class="rl-swimlane-header__label">{section.label}</span>
|
<span class="rl-swimlane-header__label">{section.label}</span>
|
||||||
{#if bucketAction}
|
{#if bucketAction}
|
||||||
<span class="rl-swimlane-header__action">
|
<span class="rl-swimlane-header__action">
|
||||||
{@render bucketAction(section.key)}
|
{@render bucketAction(section.key)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- Window each section's rows so a large grouped list (e.g. a big
|
<!-- Window each section's rows so a large grouped list (e.g. a big
|
||||||
trash, grouped by remaining days) doesn't mount every row. -->
|
trash, grouped by remaining days) doesn't mount every row. -->
|
||||||
<VirtualList items={section.rows} rowHeight={56} key={(e) => e.id} {row} />
|
<VirtualList items={section.rows} rowHeight={56} key={(e) => e.id} {row} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if grouped}
|
{:else if grouped}
|
||||||
<!-- Grouped GRID: a vertical stack of (header + its own windowed card
|
<!-- Grouped GRID: a vertical stack of (header + its own windowed card
|
||||||
grid) per section. The outer is a flex column, NOT `.files-grid-view`
|
grid) per section. The outer is a flex column, NOT `.files-grid-view`
|
||||||
(which is itself a grid and would place each header/VirtualList into a
|
(which is itself a grid and would place each header/VirtualList into a
|
||||||
cell) — the grid lives on each VirtualList's inner window via
|
cell) — the grid lives on each VirtualList's inner window via
|
||||||
`windowClass`, exactly like the flat-grid arm. This was the last
|
`windowClass`, exactly like the flat-grid arm. This was the last
|
||||||
unwindowed path: a grouped-by-default grid (trash) mounted every card
|
unwindowed path: a grouped-by-default grid (trash) mounted every card
|
||||||
(benches/ROUND13.md §V1). -->
|
(benches/ROUND13.md §V1). -->
|
||||||
<div class="rl-grouped-grid">
|
<div class="rl-grouped-grid">
|
||||||
{#each sections as section (section.key)}
|
{#each sections as section (section.key)}
|
||||||
<div class="rl-swimlane-header rl-swimlane-header--grid" role="rowheader">
|
<div class="rl-swimlane-header rl-swimlane-header--grid" role="rowheader">
|
||||||
<span class="rl-swimlane-header__label">{section.label}</span>
|
<span class="rl-swimlane-header__label">{section.label}</span>
|
||||||
{#if bucketAction}
|
{#if bucketAction}
|
||||||
<span class="rl-swimlane-header__action">
|
<span class="rl-swimlane-header__action">
|
||||||
{@render bucketAction(section.key)}
|
{@render bucketAction(section.key)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
items={section.rows}
|
items={section.rows}
|
||||||
columns={gridCols}
|
columns={gridCols}
|
||||||
rowHeight={240}
|
rowHeight={240}
|
||||||
windowClass="files-grid-view"
|
windowClass="files-grid-view"
|
||||||
key={(e) => e.id}
|
key={(e) => e.id}
|
||||||
{row}
|
{row}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if filesStore.viewMode === 'list'}
|
{:else if filesStore.viewMode === 'list'}
|
||||||
<!-- Flat list view: only the visible rows are mounted. The spacer keeps the
|
<!-- Flat list view: only the visible rows are mounted. The spacer keeps the
|
||||||
full scroll height so the end-of-list sentinel still fires. -->
|
full scroll height so the end-of-list sentinel still fires. -->
|
||||||
<div class="files-list-view" style="--files-list-columns: {columns}">
|
<div class="files-list-view" style="--files-list-columns: {columns}">
|
||||||
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if}
|
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if}
|
||||||
<VirtualList items={visibleItems} rowHeight={56} key={(e) => e.id} {row} />
|
<VirtualList items={visibleItems} rowHeight={56} key={(e) => e.id} {row} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Grid view: the windowed list's inner element IS the card grid. -->
|
<!-- Grid view: the windowed list's inner element IS the card grid. -->
|
||||||
<VirtualList
|
<VirtualList
|
||||||
items={visibleItems}
|
items={visibleItems}
|
||||||
columns={gridCols}
|
columns={gridCols}
|
||||||
rowHeight={240}
|
rowHeight={240}
|
||||||
windowClass="files-grid-view"
|
windowClass="files-grid-view"
|
||||||
key={(e) => e.id}
|
key={(e) => e.id}
|
||||||
{row}
|
{row}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasMore}
|
{#if hasMore}
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary rl-more"
|
class="btn btn-secondary rl-more"
|
||||||
data-testid="resource-list-load-more-btn"
|
data-testid="resource-list-load-more-btn"
|
||||||
onclick={onloadmore}
|
onclick={onloadmore}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? t('common.loading', 'Loading…') : t('common.load_more', 'Load more')}
|
{loading ? t('common.loading', 'Loading…') : t('common.load_more', 'Load more')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Infinite-scroll sentinel: auto-loads the next page as it nears the viewport. -->
|
<!-- Infinite-scroll sentinel: auto-loads the next page as it nears the viewport. -->
|
||||||
<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}
|
||||||
{#if rubberband}
|
{#if rubberband}
|
||||||
<!-- Marquee selection rectangle. Positioned relative to `.rl-root`
|
<!-- Marquee selection rectangle. Positioned relative to `.rl-root`
|
||||||
(which is `position: relative`); pointer-events off so the
|
(which is `position: relative`); pointer-events off so the
|
||||||
live pointermove handler on window still sees the drag. -->
|
live pointermove handler on window still sees the drag. -->
|
||||||
<div
|
<div
|
||||||
class="rl-rubberband"
|
class="rl-rubberband"
|
||||||
style:left="{rubberband.x}px"
|
style:left="{rubberband.x}px"
|
||||||
style:top="{rubberband.y}px"
|
style:top="{rubberband.y}px"
|
||||||
style:width="{rubberband.w}px"
|
style:width="{rubberband.w}px"
|
||||||
style:height="{rubberband.h}px"
|
style:height="{rubberband.h}px"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div><!-- /.rl-root -->
|
</div>
|
||||||
|
<!-- /.rl-root -->
|
||||||
|
|
||||||
{#snippet listHeader()}
|
{#snippet listHeader()}
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SkeletonList from '$lib/components/SkeletonList.svelte';
|
|
||||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
|
||||||
import { errorMessage, errorToast } from '$lib/utils/errors';
|
import { errorMessage, errorToast } from '$lib/utils/errors';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
@@ -25,10 +23,8 @@
|
|||||||
import {
|
import {
|
||||||
deleteFile,
|
deleteFile,
|
||||||
fileDownloadUrl,
|
fileDownloadUrl,
|
||||||
fileThumbnailUrl,
|
|
||||||
moveFile,
|
moveFile,
|
||||||
renameFile,
|
renameFile,
|
||||||
thumbSizeForView,
|
|
||||||
uploadFileWithProgress
|
uploadFileWithProgress
|
||||||
} from '$lib/api/endpoints/files';
|
} from '$lib/api/endpoints/files';
|
||||||
import { folderZipUrl } from '$lib/api/endpoints/folders';
|
import { folderZipUrl } from '$lib/api/endpoints/folders';
|
||||||
@@ -850,23 +846,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the server can render a thumbnail preview for this file. Only images
|
|
||||||
* and videos have server-side thumbnails (see `ThumbnailService::is_supported_image`
|
|
||||||
* plus client-uploaded video frames); the backend does NOT rasterise PDFs or
|
|
||||||
* documents, so claiming it could left their tiles blank (the doomed <img>
|
|
||||||
* 404s and `onerror` hides it). Non-thumbnail files fall back to their colour
|
|
||||||
* type icon, which renders underneath the <img> regardless.
|
|
||||||
*/
|
|
||||||
function canThumbnail(file: FileItem): boolean {
|
|
||||||
const m = file.mime_type ?? '';
|
|
||||||
// PDF joins image/video: the client-side generator ported from
|
|
||||||
// the legacy vanilla frontend renders PDFs via pdf.js on the
|
|
||||||
// `<img onerror>` fallback path. Without this the img never
|
|
||||||
// mounts for PDFs and the fallback never fires.
|
|
||||||
return m.startsWith('image/') || m.startsWith('video/') || m === 'application/pdf';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Multi-select + batch ────────────────────────────────────────────────
|
// ── Multi-select + batch ────────────────────────────────────────────────
|
||||||
// After the ResourceList migration the row-level selection UX (shift-
|
// After the ResourceList migration the row-level selection UX (shift-
|
||||||
// range, ctrl-toggle, anchor tracking, header select-all) lives inside
|
// range, ctrl-toggle, anchor tracking, header select-all) lives inside
|
||||||
@@ -1390,26 +1369,20 @@
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visual emptiness — reflects the filtered set, not the raw listing.
|
|
||||||
// When the folder contains only dotfiles that the user has chosen to
|
|
||||||
// hide, `visibleFolders + visibleFiles` is empty and the empty state
|
|
||||||
// renders; `hiddenCount` above lets the template surface a "you're
|
|
||||||
// hiding N items" hint so users aren't confused.
|
|
||||||
const isEmpty = $derived(visibleFolders.length === 0 && visibleFiles.length === 0);
|
|
||||||
|
|
||||||
// Client-side sort (flat, Drive-style). The listing endpoint returns the
|
// Client-side sort (flat, Drive-style). The listing endpoint returns the
|
||||||
// folder contents unsorted; sorting here avoids a refetch per column click.
|
// folder contents unsorted; sorting here avoids a refetch per column click.
|
||||||
|
//
|
||||||
|
// `reversed` is the single source of truth for direction — bound to
|
||||||
|
// ResourceList's `bind:reversed` below and read by the comparators.
|
||||||
|
// The legacy `sortDir: 1 | -1` value used by the comparators is a
|
||||||
|
// read-only `$derived` off `reversed` so we don't need two-way sync
|
||||||
|
// (the previous `$state` + two `$effect` mirror was fragile — a
|
||||||
|
// programmatic write to either side triggered an update on the
|
||||||
|
// other, and eslint's `prefer-writable-derived` rightly flagged it).
|
||||||
type SortField = 'name' | 'type' | 'size' | 'modified_at' | 'created_at';
|
type SortField = 'name' | 'type' | 'size' | 'modified_at' | 'created_at';
|
||||||
let sortField = $state<SortField>('name');
|
let sortField = $state<SortField>('name');
|
||||||
let sortDir = $state<1 | -1>(1);
|
let reversed = $state(false);
|
||||||
|
const sortDir = $derived<1 | -1>(reversed ? -1 : 1);
|
||||||
function toggleSort(field: SortField) {
|
|
||||||
if (sortField === field) sortDir = (sortDir * -1) as 1 | -1;
|
|
||||||
else {
|
|
||||||
sortField = field;
|
|
||||||
sortDir = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmpFolders(a: FolderItem, b: FolderItem): number {
|
function cmpFolders(a: FolderItem, b: FolderItem): number {
|
||||||
let v: number;
|
let v: number;
|
||||||
@@ -1485,18 +1458,6 @@
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ResourceList's `reversed` is a boolean; the legacy sort uses `1 | -1`.
|
|
||||||
// Two-way binding: setting `rlReversed` writes back into `sortDir`, and
|
|
||||||
// any programmatic sort direction change (e.g. group-by picking) mirrors
|
|
||||||
// out.
|
|
||||||
let rlReversed = $state(false);
|
|
||||||
$effect(() => {
|
|
||||||
rlReversed = sortDir === -1;
|
|
||||||
});
|
|
||||||
$effect(() => {
|
|
||||||
sortDir = rlReversed ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bridge for <ResourceList>'s callbacks — the row's open/favorite/drag
|
// Bridge for <ResourceList>'s callbacks — the row's open/favorite/drag
|
||||||
// props take one item; the legacy handlers take `(kind, id, name)`.
|
// props take one item; the legacy handlers take `(kind, id, name)`.
|
||||||
function rlOnOpen(item: FileItem | FolderItem) {
|
function rlOnOpen(item: FileItem | FolderItem) {
|
||||||
@@ -1547,8 +1508,6 @@
|
|||||||
return () => window.removeEventListener('pointerdown', onDown);
|
return () => window.removeEventListener('pointerdown', onDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
const SKELETON = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
||||||
|
|
||||||
// Reload whenever the route path changes.
|
// Reload whenever the route path changes.
|
||||||
//
|
//
|
||||||
// `load()` reads several reactive signals in its sync phase
|
// `load()` reads several reactive signals in its sync phase
|
||||||
@@ -1640,7 +1599,7 @@
|
|||||||
onsystemdrop={onDrop}
|
onsystemdrop={onDrop}
|
||||||
groupBys={rlGroupBys}
|
groupBys={rlGroupBys}
|
||||||
bind:groupBy
|
bind:groupBy
|
||||||
bind:reversed={rlReversed}
|
bind:reversed
|
||||||
onreload={(orderBy) => {
|
onreload={(orderBy) => {
|
||||||
sortField = orderBy as SortField;
|
sortField = orderBy as SortField;
|
||||||
}}
|
}}
|
||||||
@@ -1735,17 +1694,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="btn btn-secondary" data-testid="files-new-folder-btn" onclick={onNewFolder}>
|
||||||
class="btn btn-secondary"
|
|
||||||
data-testid="files-new-folder-btn"
|
|
||||||
onclick={onNewFolder}
|
|
||||||
>
|
|
||||||
<Icon name="folder-plus" class="icon-mr" />
|
<Icon name="folder-plus" class="icon-mr" />
|
||||||
<span>{t('actions.new_folder', 'New folder')}</span>
|
<span>{t('actions.new_folder', 'New folder')}</span>
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet batchActions(sel)}
|
{#snippet batchActions(_sel)}
|
||||||
<button
|
<button
|
||||||
class="batch-btn"
|
class="batch-btn"
|
||||||
title={t('files.add_favorites', 'Add to favorites')}
|
title={t('files.add_favorites', 'Add to favorites')}
|
||||||
|
|||||||
Reference in New Issue
Block a user