mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(library): add deletion with cover cleanup and shared state sync
This commit is contained in:
@@ -83,9 +83,15 @@ export function useBooks(libraryId: Ref<number | null>) {
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
books.value = []
|
||||
total.value = 0
|
||||
page.value = 0
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
load(true)
|
||||
}
|
||||
|
||||
return { books, total, loading, error, search, hasMore, load, onSearch }
|
||||
return { books, total, loading, error, search, hasMore, load, onSearch, clear }
|
||||
}
|
||||
|
||||
@@ -135,9 +135,15 @@ onMounted(async () => {
|
||||
if (isEpubFormat) {
|
||||
await bookSettings.load()
|
||||
const effective = bookSettings.effective.value as EpubReaderSettings
|
||||
shouldApplyStyles.value = effective.overrideBookFormatting || bookSettings.isCustomized.value
|
||||
if (shouldApplyStyles.value) {
|
||||
if (effective.overrideBookFormatting) {
|
||||
shouldApplyStyles.value = true
|
||||
seedState(effective)
|
||||
} else if (bookSettings.isCustomized.value) {
|
||||
// Only apply the per-book delta - don't bleed global defaults like dark theme into the book
|
||||
shouldApplyStyles.value = true
|
||||
seedState(bookSettings.bookDelta.value as Partial<ReaderState>)
|
||||
} else {
|
||||
shouldApplyStyles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function useReaderSettings(bookFileId: number, format: string) {
|
||||
|
||||
return {
|
||||
effective,
|
||||
bookDelta,
|
||||
isCustomized,
|
||||
load,
|
||||
updateBookSettings,
|
||||
|
||||
@@ -28,7 +28,7 @@ const defaults: ReaderState = {
|
||||
maxBlockSize: 1440,
|
||||
justify: true,
|
||||
hyphenate: true,
|
||||
isDark: true,
|
||||
isDark: false,
|
||||
themeName: 'default',
|
||||
flow: 'paginated',
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { FolderOpen, Plus, RefreshCw, Pencil, Library } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { FolderOpen, Plus, RefreshCw, Pencil, Trash2, Library } from 'lucide-vue-next'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { api } from '@/lib/api'
|
||||
import type { Library as LibraryType, LibraryStats } from '@projectx/types'
|
||||
import LibraryCreatorModal from '@/features/library/components/LibraryCreatorModal.vue'
|
||||
import { useLibraries } from '@/features/library/composables/useLibraries'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { libraries, fetchLibraries } = useLibraries()
|
||||
|
||||
const libraries = ref<LibraryType[]>([])
|
||||
const stats = ref<Record<number, LibraryStats>>({})
|
||||
const scanning = ref<Record<number, boolean>>({})
|
||||
const scanningAll = ref(false)
|
||||
const creatorOpen = ref(false)
|
||||
const editingLibrary = ref<LibraryType | null>(null)
|
||||
|
||||
async function loadLibraries() {
|
||||
const res = await api('/api/libraries')
|
||||
if (res.ok) {
|
||||
libraries.value = await res.json()
|
||||
loadAllStats()
|
||||
}
|
||||
}
|
||||
const deletingLibrary = ref<LibraryType | null>(null)
|
||||
const deleteConfirmName = ref('')
|
||||
const deleting = ref(false)
|
||||
|
||||
async function loadAllStats() {
|
||||
await Promise.all(
|
||||
@@ -30,7 +30,10 @@ async function loadAllStats() {
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(loadLibraries)
|
||||
onMounted(async () => {
|
||||
await fetchLibraries()
|
||||
loadAllStats()
|
||||
})
|
||||
|
||||
async function scan(lib: LibraryType) {
|
||||
scanning.value[lib.id] = true
|
||||
@@ -78,7 +81,44 @@ function openEdit(lib: LibraryType) {
|
||||
async function onSaved(library: LibraryType) {
|
||||
creatorOpen.value = false
|
||||
editingLibrary.value = null
|
||||
await loadLibraries()
|
||||
await fetchLibraries()
|
||||
loadAllStats()
|
||||
}
|
||||
|
||||
function openDelete(lib: LibraryType) {
|
||||
deletingLibrary.value = lib
|
||||
deleteConfirmName.value = ''
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!deletingLibrary.value) return
|
||||
deleting.value = true
|
||||
const deletedId = deletingLibrary.value.id
|
||||
const deletedName = deletingLibrary.value.name
|
||||
try {
|
||||
const res = await api(`/api/libraries/${deletedId}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
toast.success(`"${deletedName}" deleted`)
|
||||
deletingLibrary.value = null
|
||||
await fetchLibraries()
|
||||
loadAllStats()
|
||||
// If currently viewing the deleted library, navigate to the first remaining one
|
||||
if (route.name === 'library' && Number(route.params.id) === deletedId) {
|
||||
const next = libraries.value[0]
|
||||
if (next) {
|
||||
router.replace({ name: 'library', params: { id: next.id } })
|
||||
} else {
|
||||
router.replace('/')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to delete library')
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to delete library')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
@@ -150,6 +190,13 @@ function iconComponent(iconName: string | null | undefined) {
|
||||
>
|
||||
<Pencil :size="13" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||
title="Delete library"
|
||||
@click="openDelete(lib)"
|
||||
>
|
||||
<Trash2 :size="13" />
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||
:disabled="scanning[lib.id]"
|
||||
@@ -176,5 +223,39 @@ function iconComponent(iconName: string | null | undefined) {
|
||||
@close="creatorOpen = false; editingLibrary = null"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<div v-if="deletingLibrary" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div class="bg-card border border-border rounded-xl shadow-xl w-full max-w-sm p-6">
|
||||
<h3 class="text-base font-semibold text-foreground mb-1">Delete "{{ deletingLibrary.name }}"?</h3>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
This will permanently remove all books, metadata, reading progress, bookmarks, and annotations in this library. This cannot be undone.
|
||||
</p>
|
||||
<p class="text-sm text-foreground mb-2">Type the library name to confirm:</p>
|
||||
<input
|
||||
v-model="deleteConfirmName"
|
||||
type="text"
|
||||
:placeholder="deletingLibrary.name"
|
||||
class="w-full text-sm border border-border rounded-md px-3 py-2 bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-destructive mb-4"
|
||||
@keydown.enter="deleteConfirmName === deletingLibrary.name && !deleting ? confirmDelete() : null"
|
||||
@keydown.escape="deletingLibrary = null"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-border hover:bg-muted transition-colors text-muted-foreground"
|
||||
@click="deletingLibrary = null"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium rounded-md bg-destructive text-destructive-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
:disabled="deleteConfirmName !== deletingLibrary.name || deleting"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ deleting ? 'Deleting...' : 'Delete Library' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,7 +27,7 @@ const libraryId = computed<number | null>(() => {
|
||||
|
||||
const title = computed(() => libraries.value.find((l) => l.id === libraryId.value)?.name ?? 'Library')
|
||||
|
||||
const { books, total, loading, error, search, hasMore, load, onSearch } = useBooks(libraryId)
|
||||
const { books, total, loading, error, search, hasMore, load, onSearch, clear } = useBooks(libraryId)
|
||||
|
||||
const sentinel = ref<HTMLElement | null>(null)
|
||||
let observer: IntersectionObserver | null = null
|
||||
@@ -61,7 +61,10 @@ onMounted(async () => {
|
||||
onUnmounted(() => observer?.disconnect())
|
||||
|
||||
watch(libraryId, (newId, oldId) => {
|
||||
if (newId !== null && newId !== oldId) {
|
||||
if (newId === oldId) return
|
||||
if (newId === null) {
|
||||
clear()
|
||||
} else {
|
||||
search.value = ''
|
||||
load(true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user