Fix: Library Multi-Select Patches + Cleaner Icons (#100)

This commit is contained in:
Youwes09
2026-06-14 04:58:37 -05:00
parent df9755ddf2
commit 4fc96d873d
3 changed files with 68 additions and 33 deletions
+16
View File
@@ -193,6 +193,21 @@
finally { bulkWorking = false; libraryState.exitSelect() } finally { bulkWorking = false; libraryState.exitSelect() }
} }
async function bulkRemoveFromFolder() {
const catId = Number(libraryState.tab)
if (Number.isNaN(catId)) return
bulkWorking = true
try {
await getAdapter().updateMangasCategories(
[...libraryState.selected].map(String),
[],
[catId],
)
await loadCategories()
} catch (e) { console.error(e) }
finally { bulkWorking = false; libraryState.exitSelect() }
}
async function onBulkRemove() { async function onBulkRemove() {
bulkWorking = true bulkWorking = true
try { try {
@@ -451,6 +466,7 @@
onSelectAll={() => libraryState.selectAll(libraryState.filteredItems.map(m => m.id))} onSelectAll={() => libraryState.selectAll(libraryState.filteredItems.map(m => m.id))}
onExitSelect={() => libraryState.exitSelect()} onExitSelect={() => libraryState.exitSelect()}
onBulkRemove={onBulkRemove} onBulkRemove={onBulkRemove}
onBulkRemoveFromFolder={bulkRemoveFromFolder}
onBulkMove={bulkMove} onBulkMove={bulkMove}
/> />
{/if} {/if}
+46 -32
View File
@@ -1,37 +1,40 @@
<script lang="ts"> <script lang="ts">
import { CheckSquare, Trash, Folder } from 'phosphor-svelte' import { CheckSquare, Trash, Folder, FolderPlus, FolderMinus } from 'phosphor-svelte'
import Thumbnail from '$lib/components/shared/manga/Thumbnail.svelte' import Thumbnail from '$lib/components/shared/manga/Thumbnail.svelte'
import { settingsState } from '$lib/state/settings.svelte' import { settingsState } from '$lib/state/settings.svelte'
import type { Manga, Category } from '$lib/types' import type { Manga, Category } from '$lib/types'
interface Props { interface Props {
items: Manga[] items: Manga[]
loading: boolean loading: boolean
selectMode: boolean selectMode: boolean
selected: Set<number> selected: Set<number>
tab: string tab: string
visibleCategories: Category[] visibleCategories: Category[]
bulkWorking: boolean bulkWorking: boolean
onCardClick: (e: MouseEvent, m: Manga) => void onCardClick: (e: MouseEvent, m: Manga) => void
onCardContextMenu: (e: MouseEvent, m: Manga) => void onCardContextMenu: (e: MouseEvent, m: Manga) => void
onSelectAll: () => void onSelectAll: () => void
onExitSelect: () => void onExitSelect: () => void
onBulkRemove: () => void onBulkRemove: () => void
onBulkMove: (cat: Category) => void onBulkRemoveFromFolder: () => void
onBulkMove: (cat: Category) => void
} }
let { let {
items, loading, selectMode, selected, tab, items, loading, selectMode, selected, tab,
visibleCategories, bulkWorking, visibleCategories, bulkWorking,
onCardClick, onCardContextMenu, onSelectAll, onExitSelect, onBulkRemove, onBulkMove, onCardClick, onCardContextMenu, onSelectAll, onExitSelect,
onBulkRemove, onBulkRemoveFromFolder, onBulkMove,
}: Props = $props() }: Props = $props()
const isFolderTab = $derived(tab !== 'library' && tab !== 'downloaded')
let movePanelOpen = $state(false) let movePanelOpen = $state(false)
const statsAlways = $derived(settingsState.settings.libraryStatsAlways ?? false) const statsAlways = $derived(settingsState.settings.libraryStatsAlways ?? false)
const cropCovers = $derived(settingsState.settings.libraryCropCovers ?? true) const cropCovers = $derived(settingsState.settings.libraryCropCovers ?? true)
// Virtual rendering — only mount cards up to visibleCount
const PAGE = 48 const PAGE = 48
let visibleCount = $state(PAGE) let visibleCount = $state(PAGE)
let sentinel: HTMLDivElement | undefined = $state() let sentinel: HTMLDivElement | undefined = $state()
@@ -40,7 +43,6 @@
const renderedItems = $derived(items.slice(0, visibleCount)) const renderedItems = $derived(items.slice(0, visibleCount))
const hasMore = $derived(visibleCount < items.length) const hasMore = $derived(visibleCount < items.length)
// Reset when items list changes (tab switch, filter, etc)
$effect(() => { $effect(() => {
items items
visibleCount = PAGE visibleCount = PAGE
@@ -76,12 +78,12 @@
{#if visibleCategories.length > 0} {#if visibleCategories.length > 0}
<div class="move-wrap"> <div class="move-wrap">
<button <button
class="sel-action-btn" class="sel-icon-btn"
title="Move to folder"
disabled={selected.size === 0 || bulkWorking} disabled={selected.size === 0 || bulkWorking}
onclick={() => movePanelOpen = !movePanelOpen} onclick={() => movePanelOpen = !movePanelOpen}
> >
<Folder size={13} weight="bold" /> <FolderPlus size={14} weight="bold" />
Move to folder
</button> </button>
{#if movePanelOpen} {#if movePanelOpen}
<div class="move-panel" role="menu"> <div class="move-panel" role="menu">
@@ -100,13 +102,24 @@
</div> </div>
{/if} {/if}
{#if isFolderTab}
<button
class="sel-icon-btn"
title="Remove from folder"
disabled={selected.size === 0 || bulkWorking}
onclick={onBulkRemoveFromFolder}
>
<FolderMinus size={14} weight="bold" />
</button>
{/if}
<button <button
class="sel-action-btn sel-danger" class="sel-icon-btn sel-icon-danger"
title="Remove from library"
disabled={selected.size === 0 || bulkWorking} disabled={selected.size === 0 || bulkWorking}
onclick={onBulkRemove} onclick={onBulkRemove}
> >
<Trash size={13} weight="bold" /> <Trash size={14} weight="bold" />
Remove
</button> </button>
</div> </div>
</div> </div>
@@ -211,30 +224,31 @@
transition: color var(--t-base); transition: color var(--t-base);
} }
.sel-text-btn:hover { color: var(--text-primary); } .sel-text-btn:hover { color: var(--text-primary); }
.sel-action-btn { .sel-icon-btn {
display: flex; align-items: center; gap: 5px; display: flex; align-items: center; justify-content: center;
font-family: var(--font-ui); font-size: var(--text-xs); width: 28px; height: 28px; border-radius: var(--radius-md);
padding: 5px 10px; border-radius: var(--radius-md);
border: 1px solid var(--border-dim); background: var(--bg-raised); border: 1px solid var(--border-dim); background: var(--bg-raised);
color: var(--text-muted); cursor: pointer; white-space: nowrap; color: var(--text-muted); cursor: pointer; flex-shrink: 0;
transition: color var(--t-base), border-color var(--t-base), background var(--t-base); transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
} }
.sel-action-btn:disabled { opacity: 0.35; cursor: not-allowed; } .sel-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.sel-action-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); } .sel-icon-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
.sel-danger:hover:not(:disabled) { .sel-icon-danger:hover:not(:disabled) {
color: var(--color-error, #e05c5c); color: var(--color-error, #e05c5c);
border-color: color-mix(in srgb, var(--color-error, #e05c5c) 40%, transparent); border-color: color-mix(in srgb, var(--color-error, #e05c5c) 40%, transparent);
background: color-mix(in srgb, var(--color-error, #e05c5c) 8%, transparent); background: color-mix(in srgb, var(--color-error, #e05c5c) 8%, transparent);
} }
.move-wrap { position: relative; } .move-wrap { position: relative; }
.move-panel { .move-panel {
position: absolute; top: calc(100% + 4px); right: 0; z-index: 9999;
min-width: 180px; background: var(--bg-raised); min-width: 180px; background: var(--bg-raised);
border: 1px solid var(--border-base); border-radius: var(--radius-lg); border: 1px solid var(--border-base); border-radius: var(--radius-lg);
padding: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.5); padding: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.5);
animation: fadeIn 0.1s ease both; animation: fadeIn 0.1s ease both;
position: absolute; top: calc(100% + 4px); right: 0; z-index: 9999;
} }
.move-item { .move-item {
display: flex; align-items: center; gap: var(--sp-2); display: flex; align-items: center; gap: var(--sp-2);
width: 100%; padding: 7px 10px; border-radius: var(--radius-sm); width: 100%; padding: 7px 10px; border-radius: var(--radius-sm);
@@ -137,7 +137,12 @@
selectedMatch = { manga: m, chapters, readCount: matchReadCount, similarity }; selectedMatch = { manga: m, chapters, readCount: matchReadCount, similarity };
step = "confirm"; step = "confirm";
} catch (e: any) { } catch (e: any) {
error = e.message; if (/no chapters found/i.test(e.message)) {
selectedMatch = { manga: m, chapters: [], readCount: 0, similarity };
step = "confirm";
} else {
error = e.message;
}
} finally { } finally {
loadingMatchId = null; loadingMatchId = null;
} }