mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
style(reader): unify viewer chrome with dark header/footer and settings
This commit is contained in:
@@ -353,3 +353,12 @@ html.radius-rounded {
|
||||
@apply bg-background text-foreground font-sans antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.viewer-btn {
|
||||
@apply flex items-center justify-center w-8 h-8 rounded text-white/60 hover:text-white hover:bg-white/10 transition-colors shrink-0 disabled:opacity-30 disabled:cursor-not-allowed;
|
||||
}
|
||||
.viewer-sep {
|
||||
@apply w-px h-5 mx-1 bg-white/15 shrink-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +236,6 @@ function navigateSearch(cfiTarget: string) {
|
||||
<ReaderHeader
|
||||
:chapterTitle="chapterTitle"
|
||||
:isBookmarked="bookmarks.isCurrentCfiBookmarked.value"
|
||||
:isDark="isDark"
|
||||
:bgColor="activeMode.bg"
|
||||
:fgColor="activeMode.fg"
|
||||
class="transition-all duration-300"
|
||||
:class="headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-full pointer-events-none'"
|
||||
@back="router.back()"
|
||||
@@ -275,8 +272,6 @@ function navigateSearch(cfiTarget: string) {
|
||||
:sectionIndex="sectionIndex"
|
||||
:totalSections="totalSections"
|
||||
:sectionFractions="sectionFractions"
|
||||
:bgColor="activeMode.bg"
|
||||
:fgColor="activeMode.fg"
|
||||
class="transition-all duration-300"
|
||||
:class="footerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-full pointer-events-none'"
|
||||
@prevSection="goToSection(sectionIndex - 1)"
|
||||
|
||||
@@ -208,13 +208,21 @@ onUnmounted(() => {
|
||||
class="absolute top-0 inset-x-0 z-50 transition-all duration-300"
|
||||
:class="headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-full pointer-events-none'"
|
||||
>
|
||||
<div class="h-12 flex items-center gap-2 px-3" style="background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px)">
|
||||
<button class="cbz-btn" @click="router.back()"><ArrowLeft :size="16" /></button>
|
||||
<div
|
||||
class="h-12 flex items-center gap-2 px-3"
|
||||
style="
|
||||
background: rgba(18, 18, 20, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
"
|
||||
>
|
||||
<button class="viewer-btn" @click="router.back()"><ArrowLeft :size="16" /></button>
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-center">
|
||||
<span v-if="bookTitle" class="text-sm text-white/90 truncate leading-tight">{{ bookTitle }}</span>
|
||||
<span class="text-xs text-white/40 tabular-nums">{{ currentPage + 1 }} / {{ pageCount }}</span>
|
||||
</div>
|
||||
<button class="cbz-btn" :class="showSettings ? 'bg-white/20 text-white' : ''" @click="showSettings = !showSettings">
|
||||
<button class="viewer-btn" :class="showSettings ? 'bg-white/20 text-white' : ''" @click="showSettings = !showSettings">
|
||||
<Settings :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -275,9 +283,17 @@ onUnmounted(() => {
|
||||
class="absolute bottom-0 inset-x-0 z-50 transition-all duration-300"
|
||||
:class="footerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-full pointer-events-none'"
|
||||
>
|
||||
<div class="h-16 flex items-center gap-3 px-4" style="background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px)">
|
||||
<button class="cbz-btn" title="First page" @click="goToPage(0)"><ChevronsLeft :size="16" /></button>
|
||||
<button class="cbz-btn" :disabled="currentPage === 0" @click="prevPage"><ChevronLeft :size="16" /></button>
|
||||
<div
|
||||
class="h-14 flex items-center gap-3 px-4"
|
||||
style="
|
||||
background: rgba(18, 18, 20, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
"
|
||||
>
|
||||
<button class="viewer-btn" title="First page" @click="goToPage(0)"><ChevronsLeft :size="16" /></button>
|
||||
<button class="viewer-btn" :disabled="currentPage === 0" @click="prevPage"><ChevronLeft :size="16" /></button>
|
||||
|
||||
<div class="flex-1 flex flex-col justify-center gap-0.5">
|
||||
<input
|
||||
@@ -294,8 +310,8 @@ onUnmounted(() => {
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<button class="cbz-btn" :disabled="currentPage >= pageCount - 1" @click="nextPage"><ChevronRight :size="16" /></button>
|
||||
<button class="cbz-btn" title="Last page" @click="goToPage(pageCount - 1)"><ChevronsRight :size="16" /></button>
|
||||
<button class="viewer-btn" :disabled="currentPage >= pageCount - 1" @click="nextPage"><ChevronRight :size="16" /></button>
|
||||
<button class="viewer-btn" title="Last page" @click="goToPage(pageCount - 1)"><ChevronsRight :size="16" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -337,10 +353,3 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.cbz-btn {
|
||||
@apply flex items-center justify-center w-8 h-8 rounded text-white/60 hover:text-white hover:bg-white/10 transition-colors shrink-0 disabled:opacity-30 disabled:cursor-not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import { AlignJustify, Image, Palette, X } from 'lucide-vue-next'
|
||||
import type { BgColor, Direction, FitMode, ScrollMode, ViewMode } from '../composables/useCbzSettings'
|
||||
|
||||
defineProps<{
|
||||
@@ -19,6 +20,9 @@ const emit = defineEmits<{
|
||||
'update:bgColor': [v: BgColor]
|
||||
}>()
|
||||
|
||||
type Tab = 'view' | 'scroll' | 'display'
|
||||
const activeTab = ref<Tab>('view')
|
||||
|
||||
const FIT_OPTIONS: { value: FitMode; label: string }[] = [
|
||||
{ value: 'fit-page', label: 'Page' },
|
||||
{ value: 'fit-width', label: 'Width' },
|
||||
@@ -34,89 +38,158 @@ const SCROLL_OPTIONS: { value: ScrollMode; label: string }[] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 z-50 w-64 overflow-y-auto"
|
||||
style="background: rgba(18, 18, 18, 0.97); border-left: 1px solid rgba(255, 255, 255, 0.08)"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<span class="text-sm font-medium text-white">Settings</span>
|
||||
<button class="cbz-btn w-7 h-7" @click="emit('close')"><X :size="14" /></button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col gap-5">
|
||||
<!-- Fit Mode -->
|
||||
<div>
|
||||
<p class="text-xs text-white/40 mb-2 uppercase tracking-wide">Fit</p>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<div class="fixed inset-0 z-50 flex flex-col justify-end" @click.self="emit('close')">
|
||||
<div
|
||||
class="bg-card text-card-foreground rounded-t-xl max-h-[85vh] overflow-y-auto shadow-2xl border-t border-border w-full max-w-2xl mx-auto"
|
||||
@click.stop
|
||||
>
|
||||
<div class="sticky top-0 bg-card z-10 flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="opt in FIT_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="setting-btn"
|
||||
:class="fitMode === opt.value ? 'active' : ''"
|
||||
@click="emit('update:fitMode', opt.value)"
|
||||
v-for="tab in [
|
||||
{ id: 'view', icon: Image, label: 'View' },
|
||||
{ id: 'scroll', icon: AlignJustify, label: 'Scroll' },
|
||||
{ id: 'display', icon: Palette, label: 'Display' },
|
||||
] as const"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
||||
:class="activeTab === tab.id ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
{{ opt.label }}
|
||||
<component :is="tab.icon" :size="14" />
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- View Mode -->
|
||||
<div>
|
||||
<p class="text-xs text-white/40 mb-2 uppercase tracking-wide">View</p>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<button class="setting-btn" :class="viewMode === 'single' ? 'active' : ''" @click="emit('update:viewMode', 'single')">Single</button>
|
||||
<button class="setting-btn" :class="viewMode === 'two-page' ? 'active' : ''" @click="emit('update:viewMode', 'two-page')">Two-Page</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 py-5 space-y-6">
|
||||
<template v-if="activeTab === 'view'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Fit Mode</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="opt in FIT_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
fitMode === opt.value
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:fitMode', opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Pages</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
viewMode === 'single'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:viewMode', 'single')"
|
||||
>
|
||||
Single
|
||||
</button>
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
viewMode === 'two-page'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:viewMode', 'two-page')"
|
||||
>
|
||||
Two Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Scroll Mode -->
|
||||
<div>
|
||||
<p class="text-xs text-white/40 mb-2 uppercase tracking-wide">Scroll</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<button
|
||||
v-for="opt in SCROLL_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="setting-btn"
|
||||
:class="scrollMode === opt.value ? 'active' : ''"
|
||||
@click="emit('update:scrollMode', opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="activeTab === 'scroll'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Scroll Mode</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="opt in SCROLL_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
scrollMode === opt.value
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:scrollMode', opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Reading Direction</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
direction === 'ltr'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:direction', 'ltr')"
|
||||
>
|
||||
LTR →
|
||||
</button>
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
direction === 'rtl'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:direction', 'rtl')"
|
||||
>
|
||||
← RTL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Direction -->
|
||||
<div>
|
||||
<p class="text-xs text-white/40 mb-2 uppercase tracking-wide">Direction</p>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<button class="setting-btn" :class="direction === 'ltr' ? 'active' : ''" @click="emit('update:direction', 'ltr')">LTR →</button>
|
||||
<button class="setting-btn" :class="direction === 'rtl' ? 'active' : ''" @click="emit('update:direction', 'rtl')">← RTL</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background -->
|
||||
<div>
|
||||
<p class="text-xs text-white/40 mb-2 uppercase tracking-wide">Background</p>
|
||||
<div class="grid grid-cols-3 gap-1">
|
||||
<button class="setting-btn" :class="bgColor === 'black' ? 'active' : ''" @click="emit('update:bgColor', 'black')">Black</button>
|
||||
<button class="setting-btn" :class="bgColor === 'gray' ? 'active' : ''" @click="emit('update:bgColor', 'gray')">Gray</button>
|
||||
<button class="setting-btn" :class="bgColor === 'white' ? 'active' : ''" @click="emit('update:bgColor', 'white')">White</button>
|
||||
</div>
|
||||
<template v-if="activeTab === 'display'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Background</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="opt in [
|
||||
{ value: 'black', label: 'Black' },
|
||||
{ value: 'gray', label: 'Gray' },
|
||||
{ value: 'white', label: 'White' },
|
||||
] as const"
|
||||
:key="opt.value"
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
bgColor === opt.value
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:bgColor', opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.cbz-btn {
|
||||
@apply flex items-center justify-center w-8 h-8 rounded text-white/60 hover:text-white hover:bg-white/10 transition-colors shrink-0;
|
||||
}
|
||||
.setting-btn {
|
||||
@apply px-2 py-1.5 rounded text-xs text-white/60 hover:text-white hover:bg-white/10 transition-colors text-center;
|
||||
}
|
||||
.setting-btn.active {
|
||||
@apply bg-blue-500/20 text-blue-400 hover:bg-blue-500/30;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight, LayoutGrid, Maximize, Minimize, Minus, Plus, RotateCw, Search, X } from 'lucide-vue-next'
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight, Maximize, Minimize, Minus, Plus, Search, Settings, X } from 'lucide-vue-next'
|
||||
import { usePdf } from '../composables/usePdf'
|
||||
import { usePdfZoom, ZOOM_PRESETS } from '../composables/usePdfZoom'
|
||||
import { usePdfLayout, PAGE_GAP } from '../composables/usePdfLayout'
|
||||
import { usePdfRenderer } from '../composables/usePdfRenderer'
|
||||
import { useReaderProgress } from '../composables/useReaderProgress'
|
||||
import { useVisibility } from '../composables/useVisibility'
|
||||
import PdfSettingsPanel from './PdfSettingsPanel.vue'
|
||||
import type { PageDim } from '../composables/usePdf'
|
||||
|
||||
const props = defineProps<{ bookId: number; fileId: number }>()
|
||||
@@ -93,6 +94,9 @@ function toggleFullscreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings ─────────────────────────────────────────────────────────────────
|
||||
const showSettings = ref(false)
|
||||
|
||||
// ── Find ──────────────────────────────────────────────────────────────────────
|
||||
const showFind = ref(false)
|
||||
const findQuery = ref('')
|
||||
@@ -115,37 +119,6 @@ function closeFind() {
|
||||
findQuery.value = ''
|
||||
}
|
||||
|
||||
// ── Menus (zoom + view dropdowns) ────────────────────────────────────────────
|
||||
const showZoomMenu = ref(false)
|
||||
const showViewMenu = ref(false)
|
||||
const zoomBtnRef = ref<HTMLElement | null>(null)
|
||||
const viewBtnRef = ref<HTMLElement | null>(null)
|
||||
const zoomMenuStyle = ref({ top: '0px', left: '0px' })
|
||||
const viewMenuStyle = ref({ top: '0px', right: '0px' })
|
||||
|
||||
function openZoomMenu() {
|
||||
if (zoomBtnRef.value) {
|
||||
const r = zoomBtnRef.value.getBoundingClientRect()
|
||||
zoomMenuStyle.value = { top: `${r.bottom + 4}px`, left: `${r.left + r.width / 2}px` }
|
||||
}
|
||||
showZoomMenu.value = !showZoomMenu.value
|
||||
showViewMenu.value = false
|
||||
}
|
||||
|
||||
function openViewMenu() {
|
||||
if (viewBtnRef.value) {
|
||||
const r = viewBtnRef.value.getBoundingClientRect()
|
||||
viewMenuStyle.value = { top: `${r.bottom + 4}px`, right: `${window.innerWidth - r.right}px` }
|
||||
}
|
||||
showViewMenu.value = !showViewMenu.value
|
||||
showZoomMenu.value = false
|
||||
}
|
||||
|
||||
function closeAllMenus() {
|
||||
showZoomMenu.value = false
|
||||
showViewMenu.value = false
|
||||
}
|
||||
|
||||
// ── Keyboard ──────────────────────────────────────────────────────────────────
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (showFind.value && (e.target as HTMLElement)?.tagName === 'INPUT') return
|
||||
@@ -167,7 +140,7 @@ function onKeyDown(e: KeyboardEvent) {
|
||||
showFind.value = true
|
||||
} else if (e.key === 'Escape') {
|
||||
closeFind()
|
||||
closeAllMenus()
|
||||
showSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +203,7 @@ const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.v
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-[#525659] flex flex-col overflow-hidden select-none" @mousemove="showHeader()" @click="closeAllMenus()">
|
||||
<div class="fixed inset-0 bg-[#525659] flex flex-col overflow-hidden select-none" @mousemove="showHeader()">
|
||||
<!-- ── Header ─────────────────────────────────────────────────────────── -->
|
||||
<div
|
||||
class="absolute top-0 left-0 right-0 z-50 flex flex-col transition-all duration-300"
|
||||
@@ -238,13 +211,18 @@ const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.v
|
||||
>
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
class="h-12 flex items-center px-2 gap-0.5"
|
||||
style="background: rgba(50, 54, 57, 0.96); backdrop-filter: blur(8px); border-bottom: 1px solid rgba(255, 255, 255, 0.08)"
|
||||
class="h-12 flex items-center px-3 gap-0.5"
|
||||
style="
|
||||
background: rgba(18, 18, 20, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
"
|
||||
>
|
||||
<button class="pdf-btn" @click="router.back()" title="Back"><ArrowLeft :size="16" /></button>
|
||||
<div class="sep" />
|
||||
<button class="viewer-btn" @click="router.back()" title="Back"><ArrowLeft :size="16" /></button>
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<button class="pdf-btn" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)" title="Previous page">
|
||||
<button class="viewer-btn" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)" title="Previous page">
|
||||
<ChevronLeft :size="16" />
|
||||
</button>
|
||||
<div class="flex items-center gap-1 text-sm text-white/80 shrink-0">
|
||||
@@ -260,55 +238,37 @@ const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.v
|
||||
<span class="text-white/40">/</span>
|
||||
<span class="tabular-nums">{{ totalPages }}</span>
|
||||
</div>
|
||||
<button class="pdf-btn" :disabled="currentPage >= totalPages" @click="goToPage(currentPage + 1)" title="Next page">
|
||||
<button class="viewer-btn" :disabled="currentPage >= totalPages" @click="goToPage(currentPage + 1)" title="Next page">
|
||||
<ChevronRight :size="16" />
|
||||
</button>
|
||||
<div class="sep" />
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<button class="pdf-btn" @click.stop="adjustZoom(-0.1)" title="Zoom out"><Minus :size="14" /></button>
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
ref="zoomBtnRef"
|
||||
class="flex items-center gap-1 px-2 py-1 rounded text-xs text-white/80 hover:text-white hover:bg-white/10 transition-colors min-w-[72px] justify-center font-mono"
|
||||
@click.stop="openZoomMenu"
|
||||
title="Zoom level"
|
||||
>
|
||||
{{ zoomLabel }}
|
||||
<span class="text-white/30 text-[10px]">▾</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="pdf-btn" @click.stop="adjustZoom(0.1)" title="Zoom in"><Plus :size="14" /></button>
|
||||
<div class="sep" />
|
||||
<button class="viewer-btn" @click.stop="adjustZoom(-0.1)" title="Zoom out"><Minus :size="14" /></button>
|
||||
<span class="text-xs text-white/80 font-mono min-w-[64px] text-center tabular-nums select-none">{{ zoomLabel }}</span>
|
||||
<button class="viewer-btn" @click.stop="adjustZoom(0.1)" title="Zoom in"><Plus :size="14" /></button>
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
ref="viewBtnRef"
|
||||
class="flex items-center gap-1 px-2 py-1 rounded text-xs text-white/80 hover:text-white hover:bg-white/10 transition-colors"
|
||||
@click.stop="openViewMenu"
|
||||
title="View options"
|
||||
>
|
||||
<LayoutGrid :size="14" />
|
||||
<span class="text-white/30 text-[10px]">▾</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sep" />
|
||||
|
||||
<button class="pdf-btn" :class="showFind ? 'bg-white/20 text-white' : ''" @click="showFind = !showFind" title="Find (⌘F)">
|
||||
<button class="viewer-btn" :class="showFind ? 'bg-white/20 text-white' : ''" @click="showFind = !showFind" title="Find (⌘F)">
|
||||
<Search :size="14" />
|
||||
</button>
|
||||
<div class="sep" />
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<button class="pdf-btn" @click="toggleFullscreen" :title="isFullscreen ? 'Exit fullscreen' : 'Fullscreen'">
|
||||
<button class="viewer-btn" @click="toggleFullscreen" :title="isFullscreen ? 'Exit fullscreen' : 'Fullscreen'">
|
||||
<Minimize v-if="isFullscreen" :size="14" />
|
||||
<Maximize v-else :size="14" />
|
||||
</button>
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<button class="viewer-btn" :class="showSettings ? 'bg-white/20 text-white' : ''" @click.stop="showSettings = !showSettings" title="Settings">
|
||||
<Settings :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Find bar -->
|
||||
<div
|
||||
v-if="showFind"
|
||||
class="flex items-center gap-2 px-3 py-2"
|
||||
style="background: rgba(50, 54, 57, 0.98); border-bottom: 1px solid rgba(255, 255, 255, 0.08)"
|
||||
style="background: rgba(18, 18, 20, 0.98); border-bottom: 1px solid rgba(255, 255, 255, 0.08)"
|
||||
>
|
||||
<Search :size="13" class="text-white/40 shrink-0" />
|
||||
<input
|
||||
@@ -320,7 +280,7 @@ const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.v
|
||||
@keydown.escape="showFind = false"
|
||||
/>
|
||||
<span v-if="findQuery" class="text-xs text-white/40 tabular-nums shrink-0">{{ findMatches }} match{{ findMatches !== 1 ? 'es' : '' }}</span>
|
||||
<button class="pdf-btn w-6 h-6" @click="closeFind"><X :size="12" /></button>
|
||||
<button class="viewer-btn w-6 h-6" @click="closeFind"><X :size="12" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -402,116 +362,19 @@ const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.v
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zoom dropdown — teleported to escape stacking context created by header -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showZoomMenu"
|
||||
class="fixed w-40 rounded-lg shadow-2xl py-1 overflow-hidden"
|
||||
:style="{
|
||||
zIndex: 9999,
|
||||
background: 'rgba(35,38,41,0.98)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
transform: 'translateX(-50%)',
|
||||
top: zoomMenuStyle.top,
|
||||
left: zoomMenuStyle.left,
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-for="preset in ZOOM_PRESETS"
|
||||
:key="preset.label"
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="zoomMode === 'custom' && customScale === parseFloat(preset.value) ? 'text-white' : 'text-white/60'"
|
||||
@click="applyZoomPreset(preset.value)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
<span v-if="zoomMode === 'custom' && customScale === parseFloat(preset.value)" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- View dropdown — teleported to escape stacking context created by header -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showViewMenu"
|
||||
class="fixed w-44 rounded-lg shadow-2xl py-1 overflow-hidden"
|
||||
:style="{
|
||||
zIndex: 9999,
|
||||
background: 'rgba(35,38,41,0.98)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
top: viewMenuStyle.top,
|
||||
right: viewMenuStyle.right,
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="zoomMode === 'fit-width' ? 'text-white' : 'text-white/60'"
|
||||
@click="applyZoomPreset('fit-width'); showViewMenu = false"
|
||||
>
|
||||
Fit Width<span v-if="zoomMode === 'fit-width'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="zoomMode === 'fit-page' ? 'text-white' : 'text-white/60'"
|
||||
@click="applyZoomPreset('fit-page'); showViewMenu = false"
|
||||
>
|
||||
Fit Page<span v-if="zoomMode === 'fit-page'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="zoomMode === 'custom' && customScale === 1 ? 'text-white' : 'text-white/60'"
|
||||
@click="applyZoomPreset('1'); showViewMenu = false"
|
||||
>
|
||||
Actual Size<span v-if="zoomMode === 'custom' && customScale === 1" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<div class="my-1 border-t border-white/10" />
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs text-white/60 hover:text-white hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
@click="rotate(); showViewMenu = false"
|
||||
>
|
||||
Rotate CW <RotateCw :size="12" />
|
||||
</button>
|
||||
<div class="my-1 border-t border-white/10" />
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="spread === 'none' ? 'text-white' : 'text-white/60'"
|
||||
@click="spread = 'none'; showViewMenu = false"
|
||||
>
|
||||
Single Page<span v-if="spread === 'none'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="spread === 'odd' ? 'text-white' : 'text-white/60'"
|
||||
@click="spread = 'odd'; showViewMenu = false"
|
||||
>
|
||||
Two-Page Spread<span v-if="spread === 'odd'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<div class="my-1 border-t border-white/10" />
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="scrollMode === 'continuous' ? 'text-white' : 'text-white/60'"
|
||||
@click="scrollMode = 'continuous'; showViewMenu = false"
|
||||
>
|
||||
Continuous Scroll<span v-if="scrollMode === 'continuous'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1.5 text-xs hover:bg-white/10 transition-colors flex items-center justify-between"
|
||||
:class="scrollMode === 'page' ? 'text-white' : 'text-white/60'"
|
||||
@click="scrollMode = 'page'; showViewMenu = false"
|
||||
>
|
||||
Page by Page<span v-if="scrollMode === 'page'" class="text-blue-400 text-[10px]">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- Settings panel -->
|
||||
<PdfSettingsPanel
|
||||
v-if="showSettings"
|
||||
:zoomMode="zoomMode"
|
||||
:customScale="customScale"
|
||||
:zoomLabel="zoomLabel"
|
||||
:spread="spread"
|
||||
:scrollMode="scrollMode"
|
||||
:rotation="rotation"
|
||||
@applyZoomPreset="applyZoomPreset"
|
||||
@update:spread="spread = $event"
|
||||
@update:scrollMode="scrollMode = $event"
|
||||
@rotate="rotate()"
|
||||
@close="showSettings = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.pdf-btn {
|
||||
@apply flex items-center justify-center w-7 h-7 rounded text-white/60 hover:text-white hover:bg-white/10 transition-colors shrink-0 disabled:opacity-30 disabled:cursor-not-allowed;
|
||||
}
|
||||
.sep {
|
||||
@apply w-px h-5 mx-1 bg-white/15 shrink-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { LayoutGrid, RotateCw, X, ZoomIn } from 'lucide-vue-next'
|
||||
import { ZOOM_PRESETS } from '../composables/usePdfZoom'
|
||||
|
||||
const props = defineProps<{
|
||||
zoomMode: string
|
||||
customScale: number
|
||||
zoomLabel: string
|
||||
spread: 'none' | 'odd' | 'even'
|
||||
scrollMode: 'continuous' | 'page'
|
||||
rotation: 0 | 90 | 180 | 270
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
applyZoomPreset: [value: string]
|
||||
'update:spread': [v: 'none' | 'odd' | 'even']
|
||||
'update:scrollMode': [v: 'continuous' | 'page']
|
||||
rotate: []
|
||||
}>()
|
||||
|
||||
type Tab = 'zoom' | 'layout' | 'display'
|
||||
const activeTab = ref<Tab>('zoom')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-50 flex flex-col justify-end" @click.self="emit('close')">
|
||||
<div
|
||||
class="bg-card text-card-foreground rounded-t-xl max-h-[85vh] overflow-y-auto shadow-2xl border-t border-border w-full max-w-2xl mx-auto"
|
||||
@click.stop
|
||||
>
|
||||
<div class="sticky top-0 bg-card z-10 flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="tab in [
|
||||
{ id: 'zoom', icon: ZoomIn, label: 'Zoom' },
|
||||
{ id: 'layout', icon: LayoutGrid, label: 'Layout' },
|
||||
{ id: 'display', icon: RotateCw, label: 'Display' },
|
||||
] as const"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
||||
:class="activeTab === tab.id ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<component :is="tab.icon" :size="14" />
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-5 space-y-6">
|
||||
<template v-if="activeTab === 'zoom'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Preset</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
zoomMode === 'fit-width'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('applyZoomPreset', 'fit-width')"
|
||||
>
|
||||
Fit Width
|
||||
</button>
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
zoomMode === 'fit-page'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('applyZoomPreset', 'fit-page')"
|
||||
>
|
||||
Fit Page
|
||||
</button>
|
||||
<button
|
||||
v-for="preset in ZOOM_PRESETS"
|
||||
:key="preset.label"
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
zoomMode === 'custom' && customScale === parseFloat(preset.value)
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('applyZoomPreset', preset.value)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab === 'layout'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Scroll Mode</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
scrollMode === 'continuous'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:scrollMode', 'continuous')"
|
||||
>
|
||||
Continuous
|
||||
</button>
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
scrollMode === 'page'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:scrollMode', 'page')"
|
||||
>
|
||||
Page by Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Page Spread</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
spread === 'none'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:spread', 'none')"
|
||||
>
|
||||
Single Page
|
||||
</button>
|
||||
<button
|
||||
class="py-2.5 rounded-xl text-sm font-medium border-2 transition-colors"
|
||||
:class="
|
||||
spread !== 'none'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground'
|
||||
"
|
||||
@click="emit('update:spread', 'odd')"
|
||||
>
|
||||
Two-Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab === 'display'">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Rotation</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium">Rotate Clockwise</p>
|
||||
<p class="text-xs text-muted-foreground">Current: {{ rotation }}°</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium border-2 border-border hover:border-muted-foreground/40 hover:bg-muted text-foreground transition-colors"
|
||||
@click="emit('rotate')"
|
||||
>
|
||||
<RotateCw :size="16" />
|
||||
Rotate 90°
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,8 +6,6 @@ const props = defineProps<{
|
||||
sectionIndex: number
|
||||
totalSections: number
|
||||
sectionFractions: number[]
|
||||
bgColor: string
|
||||
fgColor: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -25,21 +23,14 @@ function onSeek(e: Event) {
|
||||
<template>
|
||||
<footer
|
||||
class="fixed bottom-0 left-0 right-0 h-14 z-50 flex items-center gap-3 px-4"
|
||||
:style="{
|
||||
background: `color-mix(in srgb, ${bgColor} 92%, transparent)`,
|
||||
color: fgColor,
|
||||
backdropFilter: 'blur(8px)',
|
||||
WebkitBackdropFilter: 'blur(8px)',
|
||||
borderTop: `1px solid color-mix(in srgb, ${fgColor} 12%, transparent)`,
|
||||
}"
|
||||
style="
|
||||
background: rgba(18, 18, 20, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 disabled:opacity-30 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
:disabled="sectionIndex === 0"
|
||||
@click="emit('prevSection')"
|
||||
title="Previous section"
|
||||
>
|
||||
<button class="viewer-btn" :disabled="sectionIndex === 0" @click="emit('prevSection')" title="Previous section">
|
||||
<ChevronLeft :size="18" />
|
||||
</button>
|
||||
|
||||
@@ -53,33 +44,22 @@ function onSeek(e: Event) {
|
||||
@input="onSeek"
|
||||
class="w-full h-1 rounded-full appearance-none cursor-pointer"
|
||||
:style="{
|
||||
accentColor: fgColor,
|
||||
background: `linear-gradient(to right, color-mix(in srgb, ${fgColor} 60%, transparent) ${fraction * 100}%, color-mix(in srgb, ${fgColor} 20%, transparent) ${fraction * 100}%)`,
|
||||
accentColor: 'rgba(255,255,255,0.8)',
|
||||
background: `linear-gradient(to right, rgba(255,255,255,0.6) ${fraction * 100}%, rgba(255,255,255,0.2) ${fraction * 100}%)`,
|
||||
}"
|
||||
/>
|
||||
<template v-for="(sf, idx) in sectionFractions" :key="idx">
|
||||
<div
|
||||
v-if="sf > 0 && sf < 1"
|
||||
class="absolute top-1/2 -translate-y-1/2 w-px h-3 pointer-events-none"
|
||||
:style="{
|
||||
left: `${sf * 100}%`,
|
||||
background: `color-mix(in srgb, ${fgColor} 35%, transparent)`,
|
||||
}"
|
||||
:style="{ left: `${sf * 100}%`, background: 'rgba(255,255,255,0.35)' }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<span class="text-xs tabular-nums shrink-0 min-w-[3rem] text-center" :style="{ color: `color-mix(in srgb, ${fgColor} 60%, transparent)` }">
|
||||
{{ Math.round(fraction * 100) }}%
|
||||
</span>
|
||||
<span class="text-xs tabular-nums shrink-0 min-w-[3rem] text-center text-white/50"> {{ Math.round(fraction * 100) }}% </span>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 disabled:opacity-30 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
:disabled="totalSections > 0 && sectionIndex >= totalSections - 1"
|
||||
@click="emit('nextSection')"
|
||||
title="Next section"
|
||||
>
|
||||
<button class="viewer-btn" :disabled="totalSections > 0 && sectionIndex >= totalSections - 1" @click="emit('nextSection')" title="Next section">
|
||||
<ChevronRight :size="18" />
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
@@ -5,9 +5,6 @@ import { ArrowLeft, BookOpen, Bookmark, BookmarkCheck, Maximize, Minimize, Searc
|
||||
defineProps<{
|
||||
chapterTitle: string
|
||||
isBookmarked: boolean
|
||||
isDark: boolean
|
||||
bgColor: string
|
||||
fgColor: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -32,74 +29,42 @@ onUnmounted(() => document.removeEventListener('fullscreenchange', onFullscreenC
|
||||
<template>
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 h-12 z-50 flex items-center px-3 gap-1"
|
||||
:style="{
|
||||
background: `color-mix(in srgb, ${bgColor} 92%, transparent)`,
|
||||
color: fgColor,
|
||||
backdropFilter: 'blur(8px)',
|
||||
WebkitBackdropFilter: 'blur(8px)',
|
||||
borderBottom: `1px solid color-mix(in srgb, ${fgColor} 12%, transparent)`,
|
||||
}"
|
||||
style="
|
||||
background: rgba(18, 18, 20, 0.92);
|
||||
color: white;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
@click="emit('back')"
|
||||
title="Go back"
|
||||
>
|
||||
<button class="viewer-btn" @click="emit('back')" title="Go back">
|
||||
<ArrowLeft :size="18" />
|
||||
</button>
|
||||
|
||||
<div class="w-px h-5 mx-1 shrink-0" :style="{ background: `color-mix(in srgb, ${fgColor} 20%, transparent)` }" />
|
||||
<div class="viewer-sep" />
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
@click="emit('toggleSidebar')"
|
||||
title="Table of contents"
|
||||
>
|
||||
<button class="viewer-btn" @click="emit('toggleSidebar')" title="Table of contents">
|
||||
<BookOpen :size="18" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: isBookmarked ? '#f87171' : fgColor }"
|
||||
@click="emit('toggleBookmark')"
|
||||
title="Toggle bookmark"
|
||||
>
|
||||
<button class="viewer-btn" :class="isBookmarked ? '!text-red-400' : ''" @click="emit('toggleBookmark')" title="Toggle bookmark">
|
||||
<BookmarkCheck v-if="isBookmarked" :size="18" />
|
||||
<Bookmark v-else :size="18" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
@click="emit('toggleSearch')"
|
||||
title="Search"
|
||||
>
|
||||
<button class="viewer-btn" @click="emit('toggleSearch')" title="Search">
|
||||
<Search :size="18" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1 min-w-0 px-3">
|
||||
<p class="text-sm font-medium truncate text-center" :style="{ color: `color-mix(in srgb, ${fgColor} 70%, transparent)` }">
|
||||
{{ chapterTitle }}
|
||||
</p>
|
||||
<p class="text-sm font-medium truncate text-center text-white/60">{{ chapterTitle }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
@click="emit('toggleSettings')"
|
||||
title="Settings"
|
||||
>
|
||||
<button class="viewer-btn" @click="emit('toggleSettings')" title="Settings">
|
||||
<Settings :size="18" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-md transition-opacity hover:opacity-70 shrink-0"
|
||||
:style="{ color: fgColor }"
|
||||
@click="emit('toggleFullscreen')"
|
||||
:title="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
|
||||
>
|
||||
<button class="viewer-btn" @click="emit('toggleFullscreen')" :title="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'">
|
||||
<Minimize v-if="isFullscreen" :size="18" />
|
||||
<Maximize v-else :size="18" />
|
||||
</button>
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1767500010285,
|
||||
"when": 1767510287823,
|
||||
"tag": "0000_init_schema",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1767500010285,
|
||||
"when": 1767510287823,
|
||||
"tag": "0001_lethal_pixie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1767500010285,
|
||||
"when": 1767510287823,
|
||||
"tag": "0002_powerful_thundra",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1767500010285,
|
||||
"when": 1767510287823,
|
||||
"tag": "0003_reading_progress_by_file",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user