Feat: Download Queue Move to Top/Bottom + Tooltip (#38)

This commit is contained in:
Youwes09
2026-04-23 20:54:57 -05:00
parent 4e6be5d9f5
commit 634d32f372
5 changed files with 186 additions and 23 deletions
@@ -1,5 +1,5 @@
<script lang="ts">
import { CircleNotch, ArrowUp, ArrowDown, ArrowClockwise, X } from "phosphor-svelte";
import { CircleNotch, ArrowUp, ArrowDown, ArrowLineUp, ArrowLineDown, ArrowClockwise, X } from "phosphor-svelte";
import Thumbnail from "@shared/manga/Thumbnail.svelte";
import ContextMenu from "@shared/ui/ContextMenu.svelte";
import type { MenuEntry } from "@shared/ui/ContextMenu.svelte";
@@ -20,18 +20,20 @@
onRemove: (chapterId: number) => void;
onRetry: (chapterId: number) => void;
onReorder: (chapterId: number, dir: "up" | "down") => void;
onReorderEdge: (chapterId: number, edge: "top" | "bottom") => void;
onSelect: (chapterId: number, e: MouseEvent | { shiftKey: boolean; ctrlKey: boolean; metaKey: boolean }) => void;
onBatchRemove: () => void;
onBatchRetry: () => void;
onBatchReorder: (dir: "up" | "down") => void;
onClearSelect: () => void;
onBatchRemove: () => void;
onBatchRetry: () => void;
onBatchReorder: (dir: "up" | "down") => void;
onBatchReorderEdge: (edge: "top" | "bottom") => void;
onClearSelect: () => void;
}
const {
item, index, isActive, isFirst, isLast, isRemoving,
isSelected, selectedCount, selectedErrorCount, batchWorking,
onRemove, onRetry, onReorder, onSelect,
onBatchRemove, onBatchRetry, onBatchReorder, onClearSelect,
onRemove, onRetry, onReorder, onReorderEdge, onSelect,
onBatchRemove, onBatchRetry, onBatchReorder, onBatchReorderEdge, onClearSelect,
}: Props = $props();
const manga = $derived(item.chapter.manga);
@@ -87,6 +89,19 @@
const entries: MenuEntry[] = [];
if (inBatch) {
entries.push({
label: `Move to top (${selectedCount})`,
icon: ArrowLineUp,
onClick: () => onBatchReorderEdge("top"),
disabled: batchWorking,
});
entries.push({
label: `Move to bottom (${selectedCount})`,
icon: ArrowLineDown,
onClick: () => onBatchReorderEdge("bottom"),
disabled: batchWorking,
});
entries.push({ separator: true });
entries.push({
label: `Move up (${selectedCount})`,
icon: ArrowUp,
@@ -127,6 +142,19 @@
});
entries.push({ separator: true });
}
entries.push({
label: "Move to top",
icon: ArrowLineUp,
onClick: () => onReorderEdge(item.chapter.id, "top"),
disabled: isFirst || isActive,
});
entries.push({
label: "Move to bottom",
icon: ArrowLineDown,
onClick: () => onReorderEdge(item.chapter.id, "bottom"),
disabled: isLast || isActive,
});
entries.push({ separator: true });
entries.push({
label: "Move up",
icon: ArrowUp,
@@ -4,26 +4,28 @@
import type { DownloadQueueItem } from "@types/index";
interface Props {
queue: DownloadQueueItem[];
loading: boolean;
isRunning: boolean;
dequeueing: Set<number>;
selected: Set<number>;
batchWorking: boolean;
onRemove: (chapterId: number) => void;
onRetry: (chapterId: number) => void;
onReorder: (chapterId: number, dir: "up" | "down") => void;
onSelect: (chapterId: number, e: MouseEvent) => void;
onClearSelect: () => void;
onBatchRemove: () => void;
onBatchRetry: () => void;
onBatchReorder: (dir: "up" | "down") => void;
queue: DownloadQueueItem[];
loading: boolean;
isRunning: boolean;
dequeueing: Set<number>;
selected: Set<number>;
batchWorking: boolean;
onRemove: (chapterId: number) => void;
onRetry: (chapterId: number) => void;
onReorder: (chapterId: number, dir: "up" | "down") => void;
onReorderEdge: (chapterId: number, edge: "top" | "bottom") => void;
onSelect: (chapterId: number, e: MouseEvent) => void;
onClearSelect: () => void;
onBatchRemove: () => void;
onBatchRetry: () => void;
onBatchReorder: (dir: "up" | "down") => void;
onBatchReorderEdge: (edge: "top" | "bottom") => void;
}
const {
queue, loading, isRunning, dequeueing, selected, batchWorking,
onRemove, onRetry, onReorder, onSelect, onClearSelect,
onBatchRemove, onBatchRetry, onBatchReorder,
onRemove, onRetry, onReorder, onReorderEdge, onSelect, onClearSelect,
onBatchRemove, onBatchRetry, onBatchReorder, onBatchReorderEdge,
}: Props = $props();
const selectedErrorCount = $derived(
@@ -39,6 +41,22 @@
<div class="empty">Queue is empty.</div>
{:else}
<div class="list">
<div class="list-header">
<div class="info-wrap">
<button class="info-btn" tabindex="-1" aria-label="Selection help">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<circle cx="6" cy="6" r="5.25" stroke="currentColor" stroke-width="1.5"/>
<rect x="5.25" y="5" width="1.5" height="3.5" rx="0.75" fill="currentColor"/>
<circle cx="6" cy="3.25" r="0.85" fill="currentColor"/>
</svg>
</button>
<div class="info-popover" role="tooltip">
<span>Click to select</span>
<span>Shift+click to range select</span>
<span>Ctrl+click to toggle</span>
</div>
</div>
</div>
{#each queue as item, i (item.chapter.id)}
<DownloadItem
{item}
@@ -54,11 +72,13 @@
{onRemove}
{onRetry}
{onReorder}
{onReorderEdge}
{onSelect}
{onClearSelect}
{onBatchRemove}
{onBatchRetry}
{onBatchReorder}
{onBatchReorderEdge}
/>
{/each}
</div>
@@ -71,6 +91,59 @@
gap: var(--sp-2);
}
.list-header {
display: flex;
justify-content: flex-end;
padding: 0 var(--sp-1);
}
.info-wrap {
position: relative;
display: flex;
align-items: center;
}
.info-btn {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
background: none;
padding: 0;
cursor: default;
color: var(--text-faint);
border-radius: var(--radius-sm);
transition: color var(--t-base);
}
.info-btn:hover { color: var(--text-muted); }
.info-popover {
position: absolute;
right: 0;
top: calc(100% + 6px);
background: var(--bg-overlay);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
padding: var(--sp-2) var(--sp-3);
display: none;
flex-direction: column;
gap: 4px;
white-space: nowrap;
z-index: 50;
pointer-events: none;
}
.info-popover span {
font-family: var(--font-ui);
font-size: var(--text-2xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
.info-wrap:hover .info-popover { display: flex; }
.empty {
display: flex;
align-items: center;
@@ -121,11 +121,13 @@
onRemove={(id) => downloadStore.dequeue(id)}
onRetry={(id) => downloadStore.retryOne(id)}
onReorder={(id, dir) => downloadStore.reorder(id, dir)}
onReorderEdge={(id, edge) => downloadStore.reorderToEdge(id, edge)}
onSelect={handleSelect}
onClearSelect={() => { downloadStore.clearSelection(); selectAnchor = null; }}
onBatchRemove={() => downloadStore.dequeueSelected()}
onBatchRetry={() => downloadStore.retrySelected()}
onBatchReorder={(dir) => downloadStore.reorderSelected(dir)}
onBatchReorderEdge={(edge) => downloadStore.reorderSelectedToEdge(edge)}
/>
</div>
</div>