add feat image zoom entrelineas
This commit is contained in:
parent
3e9dce1c53
commit
2078992da3
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useFavoritesStore } from '~/stores/favorites'
|
import { useFavoritesStore } from '~/stores/favorites'
|
||||||
import { useHistoryStore } from '~/stores/history'
|
import { useHistoryStore } from '~/stores/history'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
|
|
@ -90,6 +90,187 @@ function onToggleFavorite() {
|
||||||
duration: 1800
|
duration: 1800
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Desktop hover zoom (lente + panel magnificado) */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const zoomBoxRef = ref<HTMLElement | null>(null)
|
||||||
|
const zoomImgRef = ref<HTMLImageElement | null>(null)
|
||||||
|
const zoomActive = ref(false)
|
||||||
|
|
||||||
|
const cursorX = ref(0)
|
||||||
|
const cursorY = ref(0)
|
||||||
|
const imgW = ref(0)
|
||||||
|
const imgH = ref(0)
|
||||||
|
const imgOffsetX = ref(0)
|
||||||
|
const imgOffsetY = ref(0)
|
||||||
|
const boxRight = ref(0)
|
||||||
|
const boxLeft = ref(0)
|
||||||
|
const boxTop = ref(0)
|
||||||
|
|
||||||
|
const ZOOM_LEVEL = 2.5
|
||||||
|
const PANEL_W = 400
|
||||||
|
const PANEL_H = 400
|
||||||
|
const LENS_W = PANEL_W / ZOOM_LEVEL
|
||||||
|
const LENS_H = PANEL_H / ZOOM_LEVEL
|
||||||
|
|
||||||
|
function refreshImageRect() {
|
||||||
|
if (!zoomImgRef.value || !zoomBoxRef.value) return
|
||||||
|
const ir = zoomImgRef.value.getBoundingClientRect()
|
||||||
|
const br = zoomBoxRef.value.getBoundingClientRect()
|
||||||
|
imgW.value = ir.width
|
||||||
|
imgH.value = ir.height
|
||||||
|
imgOffsetX.value = ir.left - br.left
|
||||||
|
imgOffsetY.value = ir.top - br.top
|
||||||
|
boxRight.value = br.right
|
||||||
|
boxLeft.value = br.left
|
||||||
|
boxTop.value = br.top
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomEnter() {
|
||||||
|
refreshImageRect()
|
||||||
|
zoomActive.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomLeave() {
|
||||||
|
zoomActive.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomMove(e: MouseEvent) {
|
||||||
|
if (!zoomImgRef.value || !imgW.value) return
|
||||||
|
const r = zoomImgRef.value.getBoundingClientRect()
|
||||||
|
let x = e.clientX - r.left
|
||||||
|
let y = e.clientY - r.top
|
||||||
|
x = Math.max(LENS_W / 2, Math.min(imgW.value - LENS_W / 2, x))
|
||||||
|
y = Math.max(LENS_H / 2, Math.min(imgH.value - LENS_H / 2, y))
|
||||||
|
cursorX.value = x
|
||||||
|
cursorY.value = y
|
||||||
|
}
|
||||||
|
|
||||||
|
const lensStyle = computed(() => ({
|
||||||
|
left: `${imgOffsetX.value + cursorX.value - LENS_W / 2}px`,
|
||||||
|
top: `${imgOffsetY.value + cursorY.value - LENS_H / 2}px`,
|
||||||
|
width: `${LENS_W}px`,
|
||||||
|
height: `${LENS_H}px`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const zoomPanelStyle = computed(() => {
|
||||||
|
if (!imgW.value) return {}
|
||||||
|
const bgX = -(cursorX.value * ZOOM_LEVEL - PANEL_W / 2)
|
||||||
|
const bgY = -(cursorY.value * ZOOM_LEVEL - PANEL_H / 2)
|
||||||
|
const vw = typeof window !== 'undefined' ? window.innerWidth : 1600
|
||||||
|
const left = (boxRight.value + 16 + PANEL_W <= vw)
|
||||||
|
? boxRight.value + 16
|
||||||
|
: Math.max(8, boxLeft.value - 16 - PANEL_W)
|
||||||
|
return {
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${boxTop.value}px`,
|
||||||
|
width: `${PANEL_W}px`,
|
||||||
|
height: `${PANEL_H}px`,
|
||||||
|
backgroundImage: `url("${imageUrl.value}")`,
|
||||||
|
backgroundSize: `${imgW.value * ZOOM_LEVEL}px ${imgH.value * ZOOM_LEVEL}px`,
|
||||||
|
backgroundPosition: `${bgX}px ${bgY}px`,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(imageUrl, () => { setTimeout(refreshImageRect, 50) })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', refreshImageRect)
|
||||||
|
window.addEventListener('scroll', refreshImageRect, { passive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', refreshImageRect)
|
||||||
|
window.removeEventListener('scroll', refreshImageRect)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Mobile lightbox con pinch-to-zoom */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const lightboxOpen = ref(false)
|
||||||
|
const lightboxScale = ref(1)
|
||||||
|
const lbTranslateX = ref(0)
|
||||||
|
const lbTranslateY = ref(0)
|
||||||
|
|
||||||
|
let lbInitialDist = 0
|
||||||
|
let lbInitialScale = 1
|
||||||
|
let lbLastTouchX = 0
|
||||||
|
let lbLastTouchY = 0
|
||||||
|
let lbLastMidX = 0
|
||||||
|
let lbLastMidY = 0
|
||||||
|
|
||||||
|
const LB_MIN_SCALE = 1
|
||||||
|
const LB_MAX_SCALE = 5
|
||||||
|
|
||||||
|
function openLightbox() {
|
||||||
|
lightboxScale.value = LB_MIN_SCALE
|
||||||
|
lbTranslateX.value = 0
|
||||||
|
lbTranslateY.value = 0
|
||||||
|
lightboxOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox() {
|
||||||
|
lightboxOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTouchDist(t1: Touch, t2: Touch): number {
|
||||||
|
return Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLightboxTouchStart(e: TouchEvent) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
lbLastTouchX = e.touches[0].clientX
|
||||||
|
lbLastTouchY = e.touches[0].clientY
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
lbInitialDist = getTouchDist(e.touches[0], e.touches[1])
|
||||||
|
lbInitialScale = lightboxScale.value
|
||||||
|
lbLastMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2
|
||||||
|
lbLastMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLightboxTouchMove(e: TouchEvent) {
|
||||||
|
// Prevent page scroll while interacting inside the lightbox
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.touches.length === 1 && lightboxScale.value > 1) {
|
||||||
|
const dx = e.touches[0].clientX - lbLastTouchX
|
||||||
|
const dy = e.touches[0].clientY - lbLastTouchY
|
||||||
|
lbTranslateX.value += dx
|
||||||
|
lbTranslateY.value += dy
|
||||||
|
lbLastTouchX = e.touches[0].clientX
|
||||||
|
lbLastTouchY = e.touches[0].clientY
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
const dist = getTouchDist(e.touches[0], e.touches[1])
|
||||||
|
lightboxScale.value = Math.max(LB_MIN_SCALE, Math.min(LB_MAX_SCALE, lbInitialScale * (dist / lbInitialDist)))
|
||||||
|
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2
|
||||||
|
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||||
|
lbTranslateX.value += midX - lbLastMidX
|
||||||
|
lbTranslateY.value += midY - lbLastMidY
|
||||||
|
lbLastMidX = midX
|
||||||
|
lbLastMidY = midY
|
||||||
|
if (lightboxScale.value <= LB_MIN_SCALE) {
|
||||||
|
lbTranslateX.value = 0
|
||||||
|
lbTranslateY.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLightboxTouchEnd() {
|
||||||
|
if (lightboxScale.value <= LB_MIN_SCALE) {
|
||||||
|
lightboxScale.value = LB_MIN_SCALE
|
||||||
|
lbTranslateX.value = 0
|
||||||
|
lbTranslateY.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lightboxImgStyle = computed(() => ({
|
||||||
|
transform: `translate(${lbTranslateX.value}px, ${lbTranslateY.value}px) scale(${lightboxScale.value})`,
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -225,13 +406,47 @@ function onToggleFavorite() {
|
||||||
<div v-if="!showStudies" class="flex-1 overflow-y-auto">
|
<div v-if="!showStudies" class="flex-1 overflow-y-auto">
|
||||||
<div class="p-4 sm:p-6 pb-10 flex flex-col gap-6">
|
<div class="p-4 sm:p-6 pb-10 flex flex-col gap-6">
|
||||||
|
|
||||||
<!-- Imagen -->
|
<!-- Imagen: mobile toca para abrir lightbox, desktop zoom al hover -->
|
||||||
<div
|
<template v-if="imageUrl">
|
||||||
v-if="imageUrl"
|
<!-- Mobile: imagen con indicador de zoom táctil -->
|
||||||
class="rounded-xl overflow-hidden border border-default shadow-sm bg-elevated flex items-center justify-center"
|
<div
|
||||||
>
|
class="lg:hidden rounded-xl overflow-hidden border border-default shadow-sm bg-elevated relative cursor-pointer"
|
||||||
<img :src="imageUrl" :alt="title" loading="lazy" class="max-w-full h-auto">
|
@click="openLightbox"
|
||||||
</div>
|
>
|
||||||
|
<img :src="imageUrl" :alt="title" loading="lazy" class="w-full h-auto select-none">
|
||||||
|
<div class="absolute bottom-2 right-2 bg-black/50 rounded-full p-1.5 pointer-events-none">
|
||||||
|
<UIcon name="i-lucide-zoom-in" class="size-4 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop: hover zoom con lente -->
|
||||||
|
<div
|
||||||
|
ref="zoomBoxRef"
|
||||||
|
class="hidden lg:flex rounded-xl bg-elevated border border-default shadow-sm p-4 items-center justify-center relative cursor-zoom-in select-none min-h-48"
|
||||||
|
@mouseenter="onZoomEnter"
|
||||||
|
@mouseleave="onZoomLeave"
|
||||||
|
@mousemove="onZoomMove"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref="zoomImgRef"
|
||||||
|
:src="imageUrl"
|
||||||
|
:alt="title"
|
||||||
|
loading="lazy"
|
||||||
|
class="max-h-full max-w-full object-contain drop-shadow-sm select-none"
|
||||||
|
draggable="false"
|
||||||
|
@load="refreshImageRect"
|
||||||
|
/>
|
||||||
|
<!-- Lente: marca el área magnificada -->
|
||||||
|
<div
|
||||||
|
v-show="zoomActive"
|
||||||
|
class="absolute pointer-events-none border-2 border-white shadow-lg rounded"
|
||||||
|
style="background-color: rgba(255,255,255,0.35);"
|
||||||
|
:style="lensStyle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Sin imagen -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="rounded-xl border border-dashed border-default p-8 text-center text-xs text-dimmed"
|
class="rounded-xl border border-dashed border-default p-8 text-center text-xs text-dimmed"
|
||||||
|
|
@ -310,5 +525,41 @@ function onToggleFavorite() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel de zoom desktop: Teleport a body para escapar del stacking context -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-show="zoomActive"
|
||||||
|
class="hidden lg:block fixed pointer-events-none bg-white border border-gray-200 shadow-2xl rounded-lg overflow-hidden"
|
||||||
|
style="z-index: 9999;"
|
||||||
|
:style="zoomPanelStyle"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- Lightbox mobile con pinch-to-zoom -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="lightboxOpen"
|
||||||
|
class="lg:hidden fixed inset-0 bg-black z-[9999] flex items-center justify-center"
|
||||||
|
@touchstart.passive="onLightboxTouchStart"
|
||||||
|
@touchmove.prevent="onLightboxTouchMove"
|
||||||
|
@touchend="onLightboxTouchEnd"
|
||||||
|
@click.self="closeLightbox"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="absolute top-4 right-4 z-10 text-white bg-black/60 rounded-full p-2"
|
||||||
|
@click="closeLightbox"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-x" class="size-6" />
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
:src="imageUrl!"
|
||||||
|
:alt="title"
|
||||||
|
class="max-w-full max-h-full object-contain select-none"
|
||||||
|
:style="lightboxImgStyle"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import dayjs from 'dayjs'
|
||||||
import { useDebounce } from '@vueuse/core'
|
import { useDebounce } from '@vueuse/core'
|
||||||
import { useFavoritesStore } from '~/stores/favorites'
|
import { useFavoritesStore } from '~/stores/favorites'
|
||||||
import { useHistoryStore } from '~/stores/history'
|
import { useHistoryStore } from '~/stores/history'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSettingsStore } from '~/stores/settings'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
|
|
||||||
interface TypesenseHighlight {
|
interface TypesenseHighlight {
|
||||||
|
|
@ -67,6 +69,7 @@ const emits = defineEmits(['close'])
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const favorites = useFavoritesStore()
|
const favorites = useFavoritesStore()
|
||||||
const history = useHistoryStore()
|
const history = useHistoryStore()
|
||||||
|
const { showParagraphNumbers } = storeToRefs(useSettingsStore())
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
function toSearchHit(doc: DocumentDoc): SearchHit {
|
function toSearchHit(doc: DocumentDoc): SearchHit {
|
||||||
|
|
@ -830,7 +833,7 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
<div class="bg-white rounded-lg shadow-md p-4 mb-4 last:mb-0">
|
<div class="bg-white rounded-lg shadow-md p-4 mb-4 last:mb-0">
|
||||||
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
||||||
<div class="flex items-start gap-2 mb-2">
|
<div class="flex items-start gap-2 mb-2">
|
||||||
<div class="w-4 mr-2">
|
<div v-if="showParagraphNumbers" class="w-4 mr-2">
|
||||||
<UBadge v-if="hit.document.number" :label="`#${hit.document.number}`" size="xs" variant="outline"
|
<UBadge v-if="hit.document.number" :label="`#${hit.document.number}`" size="xs" variant="outline"
|
||||||
color="info" />
|
color="info" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const { $i18n } = useNuxtApp()
|
||||||
const t = $i18n.t
|
const t = $i18n.t
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const { pageSize, paginationType } = storeToRefs(settings)
|
const { pageSize, paginationType, showParagraphNumbers } = storeToRefs(settings)
|
||||||
|
|
||||||
const pageSizeItems = PAGE_SIZE_OPTIONS.map(n => ({
|
const pageSizeItems = PAGE_SIZE_OPTIONS.map(n => ({
|
||||||
value: n,
|
value: n,
|
||||||
|
|
@ -28,32 +28,47 @@ const paginationItems = [
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel id="settings-panel">
|
<UDashboardPanel id="settings-panel" grow>
|
||||||
<UDashboardNavbar :title="t('nav.settings')">
|
<UDashboardNavbar :title="t('nav.settings')">
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<UDashboardSidebarCollapse />
|
<UDashboardSidebarCollapse />
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<div class="overflow-y-auto flex-1 p-6 space-y-8 max-w-lg">
|
<div class="overflow-y-auto flex-1">
|
||||||
<!-- Resultados por búsqueda -->
|
<div class="p-6 space-y-8">
|
||||||
<div>
|
<!-- Resultados por búsqueda -->
|
||||||
<p class="text-sm font-semibold text-highlighted mb-1">
|
<div>
|
||||||
{{ t('settings.page_size_title') }}
|
<p class="text-sm font-semibold text-highlighted mb-1">
|
||||||
</p>
|
{{ t('settings.page_size_title') }}
|
||||||
<p class="text-xs text-muted mb-4">{{ t('settings.page_size_desc') }}</p>
|
</p>
|
||||||
<URadioGroup v-model="pageSize" :items="pageSizeItems" />
|
<p class="text-xs text-muted mb-4">{{ t('settings.page_size_desc') }}</p>
|
||||||
</div>
|
<URadioGroup v-model="pageSize" :items="pageSizeItems" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<USeparator />
|
<USeparator />
|
||||||
|
|
||||||
<!-- Tipo de paginación -->
|
<!-- Tipo de paginación -->
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-highlighted mb-1">
|
<p class="text-sm font-semibold text-highlighted mb-1">
|
||||||
{{ t('settings.pagination_title') }}
|
{{ t('settings.pagination_title') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted mb-4">{{ t('settings.pagination_desc') }}</p>
|
<p class="text-xs text-muted mb-4">{{ t('settings.pagination_desc') }}</p>
|
||||||
<URadioGroup v-model="paginationType" :items="paginationItems" />
|
<URadioGroup v-model="paginationType" :items="paginationItems" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<USeparator />
|
||||||
|
|
||||||
|
<!-- Números de párrafo -->
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-highlighted mb-1">
|
||||||
|
{{ t('settings.paragraph_numbers_title') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted">{{ t('settings.paragraph_numbers_desc') }}</p>
|
||||||
|
</div>
|
||||||
|
<USwitch v-model="showParagraphNumbers" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ const STORAGE_KEY = 'lgcc:settings:v1'
|
||||||
interface SettingsData {
|
interface SettingsData {
|
||||||
pageSize: PageSizeOption
|
pageSize: PageSizeOption
|
||||||
paginationType: PaginationType
|
paginationType: PaginationType
|
||||||
|
showParagraphNumbers: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULTS: SettingsData = {
|
const DEFAULTS: SettingsData = {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
paginationType: 'infinite_scroll'
|
paginationType: 'infinite_scroll',
|
||||||
|
showParagraphNumbers: true
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStorage(): SettingsData {
|
function readStorage(): SettingsData {
|
||||||
|
|
@ -31,7 +33,10 @@ function readStorage(): SettingsData {
|
||||||
paginationType:
|
paginationType:
|
||||||
parsed.paginationType === 'numbered' || parsed.paginationType === 'infinite_scroll'
|
parsed.paginationType === 'numbered' || parsed.paginationType === 'infinite_scroll'
|
||||||
? parsed.paginationType
|
? parsed.paginationType
|
||||||
: DEFAULTS.paginationType
|
: DEFAULTS.paginationType,
|
||||||
|
showParagraphNumbers: typeof parsed.showParagraphNumbers === 'boolean'
|
||||||
|
? parsed.showParagraphNumbers
|
||||||
|
: DEFAULTS.showParagraphNumbers
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULTS }
|
return { ...DEFAULTS }
|
||||||
|
|
@ -50,6 +55,7 @@ function writeStorage(data: SettingsData) {
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
const pageSize = ref<PageSizeOption>(DEFAULTS.pageSize)
|
const pageSize = ref<PageSizeOption>(DEFAULTS.pageSize)
|
||||||
const paginationType = ref<PaginationType>(DEFAULTS.paginationType)
|
const paginationType = ref<PaginationType>(DEFAULTS.paginationType)
|
||||||
|
const showParagraphNumbers = ref<boolean>(DEFAULTS.showParagraphNumbers)
|
||||||
const ready = ref(false)
|
const ready = ref(false)
|
||||||
let hydrated = false
|
let hydrated = false
|
||||||
|
|
||||||
|
|
@ -59,20 +65,22 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||||
const data = readStorage()
|
const data = readStorage()
|
||||||
pageSize.value = data.pageSize
|
pageSize.value = data.pageSize
|
||||||
paginationType.value = data.paginationType
|
paginationType.value = data.paginationType
|
||||||
|
showParagraphNumbers.value = data.showParagraphNumbers
|
||||||
ready.value = true
|
ready.value = true
|
||||||
hydrated = true
|
hydrated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
watch([pageSize, paginationType], () => {
|
watch([pageSize, paginationType, showParagraphNumbers], () => {
|
||||||
if (!hydrated) return
|
if (!hydrated) return
|
||||||
writeStorage({ pageSize: pageSize.value, paginationType: paginationType.value })
|
writeStorage({ pageSize: pageSize.value, paginationType: paginationType.value, showParagraphNumbers: showParagraphNumbers.value })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageSize,
|
pageSize,
|
||||||
paginationType,
|
paginationType,
|
||||||
|
showParagraphNumbers,
|
||||||
ready,
|
ready,
|
||||||
hydrate
|
hydrate
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@
|
||||||
"infinite_scroll": "Infinite scroll",
|
"infinite_scroll": "Infinite scroll",
|
||||||
"infinite_scroll_desc": "Results load automatically when you reach the end of the list.",
|
"infinite_scroll_desc": "Results load automatically when you reach the end of the list.",
|
||||||
"numbered": "Numbered pages",
|
"numbered": "Numbered pages",
|
||||||
"numbered_desc": "Navigate between pages with pagination controls."
|
"numbered_desc": "Navigate between pages with pagination controls.",
|
||||||
|
"paragraph_numbers_title": "Paragraph numbers",
|
||||||
|
"paragraph_numbers_desc": "Show the paragraph number next to each paragraph in the document detail."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@
|
||||||
"infinite_scroll": "Scroll infinito",
|
"infinite_scroll": "Scroll infinito",
|
||||||
"infinite_scroll_desc": "Los resultados se cargan automáticamente al llegar al final de la lista.",
|
"infinite_scroll_desc": "Los resultados se cargan automáticamente al llegar al final de la lista.",
|
||||||
"numbered": "Páginas numeradas",
|
"numbered": "Páginas numeradas",
|
||||||
"numbered_desc": "Navega entre páginas con controles de paginación."
|
"numbered_desc": "Navega entre páginas con controles de paginación.",
|
||||||
|
"paragraph_numbers_title": "Números de párrafo",
|
||||||
|
"paragraph_numbers_desc": "Muestra el número de párrafo al lado de cada párrafo en el detalle del documento."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@
|
||||||
"infinite_scroll": "Défilement infini",
|
"infinite_scroll": "Défilement infini",
|
||||||
"infinite_scroll_desc": "Les résultats se chargent automatiquement en fin de liste.",
|
"infinite_scroll_desc": "Les résultats se chargent automatiquement en fin de liste.",
|
||||||
"numbered": "Pages numérotées",
|
"numbered": "Pages numérotées",
|
||||||
"numbered_desc": "Naviguez entre les pages avec des contrôles de pagination."
|
"numbered_desc": "Naviguez entre les pages avec des contrôles de pagination.",
|
||||||
|
"paragraph_numbers_title": "Numéros de paragraphe",
|
||||||
|
"paragraph_numbers_desc": "Affiche le numéro de paragraphe à côté de chaque paragraphe dans le détail du document."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
"infinite_scroll": "Rolagem infinita",
|
"infinite_scroll": "Rolagem infinita",
|
||||||
"infinite_scroll_desc": "Os resultados carregam automaticamente ao chegar ao final da lista.",
|
"infinite_scroll_desc": "Os resultados carregam automaticamente ao chegar ao final da lista.",
|
||||||
"numbered": "Páginas numeradas",
|
"numbered": "Páginas numeradas",
|
||||||
"numbered_desc": "Navegue entre páginas com controles de paginação."
|
"numbered_desc": "Navegue entre páginas com controles de paginação.",
|
||||||
|
"paragraph_numbers_title": "Números de parágrafo",
|
||||||
|
"paragraph_numbers_desc": "Exibe o número do parágrafo ao lado de cada parágrafo no detalhe do documento."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue