mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(reader): redesign ebook settings as one panel in two containers
Replace the three-tab dropdown with a single scrollable column ordered by how often each setting is used. The tabs gave no clue where anything lived, so finding hyphenation or the font picker meant hunting; one column makes the whole set discoverable in a single scan. Move continuous values onto sliders. Stepping page width across its range took thirty clicks, and the labels named the variable rather than the outcome, so page width now reads Narrow through Full while still storing pixels. Text size keeps discrete buttons because it is nudged constantly and steps are what that control wants. Render each theme as a miniature page in its own colors. Thirteen near-identical dots gave no way to tell Sepia from Ember without selecting one and looking at the result. Swap the desktop container from DropdownMenu to Popover, since menu semantics capture the arrow keys the new sliders need, and add a bottom sheet below the sm breakpoint so the page stays visible while it is being tuned and the controls sit within thumb reach. Both render the same panel component; only the wrapper changes. Wire up resetBookSettings, which already existed with no caller, and disable the control when the book carries no overrides. Trim the built-in fonts to Book default, Serif, Sans-serif and Monospace. The named serifs duplicated what the generic keyword already resolves to, and Palatino fell back unpredictably away from macOS. Saved selections keep rendering because the settings sanitizer accepts any font string; they simply no longer show a selected chip. Move the theme names and built-in font labels into the locale catalogs. Both were hardcoded English and so stayed untranslated in every language.
This commit is contained in:
@@ -10,6 +10,8 @@ import SheetOverlay from './SheetOverlay.vue'
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
class?: HTMLAttributes['class']
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
/** Drops the built-in corner close button when the sheet content supplies its own header actions. */
|
||||
hideClose?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
@@ -21,7 +23,7 @@ const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
})
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'side')
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'side', 'hideClose')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
@@ -48,6 +50,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="!hideClose"
|
||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
|
||||
>
|
||||
<X class="size-4" />
|
||||
|
||||
@@ -379,6 +379,18 @@ async function applyUpdate(partial: Partial<ReaderState>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Drops this book's overrides and falls back to the reader defaults. The renderer keeps
|
||||
// injecting styles so the change is visible immediately rather than only after a reopen.
|
||||
async function resetSettings() {
|
||||
const previousSpread = state.value.fixedLayoutSpread
|
||||
bookSettings.resetBookSettings()
|
||||
shouldApplyStyles.value = true
|
||||
seedState(bookSettings.effective.value as EpubReaderSettings)
|
||||
if (state.value.fixedLayoutSpread !== previousSpread) {
|
||||
await reopenEpubAtCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => footerMode.value,
|
||||
(mode) => {
|
||||
@@ -589,7 +601,14 @@ watch(
|
||||
@startReading="startTrackedReading"
|
||||
>
|
||||
<template #settingsPanel>
|
||||
<ReaderSettingsPanel :state="state" :customFonts="customFonts" :is-fixed-layout="isFixedLayout" @update="applyUpdate" />
|
||||
<ReaderSettingsPanel
|
||||
:state="state"
|
||||
:customFonts="customFonts"
|
||||
:is-fixed-layout="isFixedLayout"
|
||||
:can-reset="bookSettings.isCustomized.value"
|
||||
@update="applyUpdate"
|
||||
@reset="resetSettings"
|
||||
/>
|
||||
</template>
|
||||
</ReaderHeader>
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
Search,
|
||||
Settings,
|
||||
} from '@lucide/vue'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useFullscreen } from '../../shared/composables/useFullscreen'
|
||||
|
||||
@@ -42,10 +44,18 @@ const emit = defineEmits<{
|
||||
|
||||
const { isFullscreen } = useFullscreen()
|
||||
|
||||
// The settings surface is one panel in two containers: an anchored popover where there
|
||||
// is room beside the text, a bottom sheet where the thumb is and the page must stay visible.
|
||||
const isCompact = useMediaQuery('(max-width: 639px)')
|
||||
|
||||
function onSettingsOpenChange(open: boolean) {
|
||||
emit('update:settingsOpen', open)
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
emit('update:settingsOpen', true)
|
||||
}
|
||||
|
||||
function getFooterModeIcon(mode: 0 | 1 | 2) {
|
||||
if (mode === 0) return FileText
|
||||
if (mode === 1) return Clock3
|
||||
@@ -159,8 +169,33 @@ function getFooterModeTooltip(mode: 0 | 1 | 2): string {
|
||||
<TooltipContent>{{ isFullscreen ? t('reader.header.exitFullscreen') : t('reader.header.enterFullscreen') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu :open="props.settingsOpen" @update:open="onSettingsOpenChange">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<template v-if="isCompact">
|
||||
<button
|
||||
class="viewer-btn"
|
||||
:class="props.settingsOpen ? '!bg-muted !text-foreground' : ''"
|
||||
:title="t('reader.settings.title')"
|
||||
:aria-label="t('reader.settings.ariaLabel')"
|
||||
@click="openSettings"
|
||||
>
|
||||
<Settings :size="18" />
|
||||
</button>
|
||||
<Sheet :open="props.settingsOpen" @update:open="onSettingsOpenChange">
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
hide-close
|
||||
class="max-h-[85vh] gap-0 rounded-t-2xl border-border bg-card p-0"
|
||||
:aria-label="t('reader.settings.ariaLabel')"
|
||||
>
|
||||
<div class="flex shrink-0 justify-center pt-2.5 pb-1">
|
||||
<div class="h-1 w-9 rounded-full bg-border" />
|
||||
</div>
|
||||
<slot name="settingsPanel" />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</template>
|
||||
|
||||
<Popover v-else :open="props.settingsOpen" @update:open="onSettingsOpenChange">
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
class="viewer-btn"
|
||||
:class="props.settingsOpen ? '!bg-muted !text-foreground' : ''"
|
||||
@@ -169,16 +204,16 @@ function getFooterModeTooltip(mode: 0 | 1 | 2): string {
|
||||
>
|
||||
<Settings :size="18" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
side="bottom"
|
||||
:side-offset="10"
|
||||
class="w-[22rem] max-w-[calc(100vw-1rem)] max-h-[min(80vh,38rem)] rounded-lg border-border bg-card p-0 shadow-2xl overflow-hidden"
|
||||
class="flex max-h-[min(80vh,40rem)] w-[21rem] max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-xl border-border bg-card p-0 shadow-2xl"
|
||||
>
|
||||
<slot name="settingsPanel" />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, useId, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BookOpen, LayoutGrid, Moon, Palette, ScrollText, Sun, Type } from '@lucide/vue'
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
LayoutGrid,
|
||||
Moon,
|
||||
RectangleHorizontal,
|
||||
RectangleVertical,
|
||||
RotateCcw,
|
||||
Rows2,
|
||||
Rows4,
|
||||
ScrollText,
|
||||
Sun,
|
||||
} from '@lucide/vue'
|
||||
import type { ReaderState } from '../composables/useReaderState'
|
||||
import type { FontFamily, useCustomFonts } from '../composables/useCustomFonts'
|
||||
import { themes } from '../constants/themes'
|
||||
import { BUILTIN_READER_FONT_OPTIONS } from '@/features/reader/shared/constants/font-options'
|
||||
import { BUILTIN_READER_FONT_OPTIONS, type ReaderBuiltInFontOption } from '@/features/reader/shared/constants/font-options'
|
||||
import { formatFontFamilyLabel } from '@/features/reader/shared/lib/font-display'
|
||||
import ReaderRangeField from '@/features/reader/shared/components/ReaderRangeField.vue'
|
||||
import ReaderSegmentedControl from '@/features/reader/shared/components/ReaderSegmentedControl.vue'
|
||||
import ToggleSwitch from '@/components/ui/ToggleSwitch.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -14,78 +29,128 @@ const props = defineProps<{
|
||||
state: ReaderState
|
||||
customFonts?: ReturnType<typeof useCustomFonts>
|
||||
isFixedLayout?: boolean
|
||||
canReset?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [partial: Partial<ReaderState>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
type Tab = 'appearance' | 'text' | 'layout'
|
||||
const FONT_SIZE_MIN = 10
|
||||
const FONT_SIZE_MAX = 32
|
||||
const COLUMN_MIN = 1
|
||||
const COLUMN_MAX = 10
|
||||
|
||||
const activeTab = ref<Tab>('appearance')
|
||||
const contentRef = ref<HTMLElement | null>(null)
|
||||
const hasTabBarShadow = ref(false)
|
||||
const scrollMemory = ref<Record<Tab, number>>({
|
||||
appearance: 0,
|
||||
text: 0,
|
||||
layout: 0,
|
||||
})
|
||||
const isScrolled = ref(false)
|
||||
|
||||
const allTabs = computed<{ id: Tab; icon: typeof Palette; label: string }[]>(() => [
|
||||
{ id: 'appearance', icon: Palette, label: t('reader.settings.tabs.appearance') },
|
||||
{ id: 'text', icon: Type, label: t('reader.settings.tabs.text') },
|
||||
{ id: 'layout', icon: LayoutGrid, label: t('reader.settings.tabs.layout') },
|
||||
])
|
||||
|
||||
const tabs = computed(() => (props.isFixedLayout ? allTabs.value.filter((tab) => tab.id !== 'text') : allTabs.value))
|
||||
|
||||
const stepperButtonClass = 'size-8 rounded-lg border border-border text-lg font-light text-foreground transition-colors hover:bg-muted'
|
||||
const stepperGroupClass = 'flex min-w-[11rem] items-center justify-end gap-2'
|
||||
const stepperValueClass = 'w-[4rem] text-center text-sm font-mono font-medium tabular-nums text-foreground'
|
||||
|
||||
function step(field: keyof ReaderState, delta: number, min: number, max: number, precision = 0) {
|
||||
const current = props.state[field] as number
|
||||
const next = Math.max(min, Math.min(max, current + delta))
|
||||
const rounded = precision > 0 ? Math.round(next * Math.pow(10, precision)) / Math.pow(10, precision) : Math.round(next)
|
||||
emit('update', { [field]: rounded } as Partial<ReaderState>)
|
||||
}
|
||||
|
||||
function formatGap(v: number) {
|
||||
return `${Math.round(v * 100)}%`
|
||||
}
|
||||
const justifyLabelId = `reader-justify-${useId()}`
|
||||
const hyphenationLabelId = `reader-hyphenation-${useId()}`
|
||||
|
||||
function onContentScroll() {
|
||||
if (!contentRef.value) return
|
||||
scrollMemory.value[activeTab.value] = contentRef.value.scrollTop
|
||||
hasTabBarShadow.value = contentRef.value.scrollTop > 0
|
||||
isScrolled.value = (contentRef.value?.scrollTop ?? 0) > 0
|
||||
}
|
||||
|
||||
function setActiveTab(tab: Tab) {
|
||||
if (tab === activeTab.value) return
|
||||
const modeOptions = computed(() => [
|
||||
{ value: 'light', label: t('reader.settings.mode.light'), icon: Sun },
|
||||
{ value: 'dark', label: t('reader.settings.mode.dark'), icon: Moon },
|
||||
])
|
||||
|
||||
if (contentRef.value) {
|
||||
scrollMemory.value[activeTab.value] = contentRef.value.scrollTop
|
||||
}
|
||||
const flowOptions = computed(() => [
|
||||
{ value: 'paginated', label: t('reader.settings.flow.paginated'), icon: BookOpen },
|
||||
{ value: 'scrolled', label: t('reader.settings.flow.scrolled'), icon: ScrollText },
|
||||
])
|
||||
|
||||
activeTab.value = tab
|
||||
const spreadOptions = computed(() => [
|
||||
{ value: 'auto', label: t('reader.settings.spread.bookDefault'), icon: LayoutGrid },
|
||||
{ value: 'none', label: t('reader.settings.spread.singlePage'), icon: BookOpen },
|
||||
])
|
||||
|
||||
nextTick(() => {
|
||||
if (!contentRef.value) return
|
||||
contentRef.value.scrollTop = scrollMemory.value[tab] ?? 0
|
||||
hasTabBarShadow.value = contentRef.value.scrollTop > 0
|
||||
})
|
||||
}
|
||||
const currentMode = computed(() => (props.state.isDark ? 'dark' : 'light'))
|
||||
|
||||
onMounted(() => {
|
||||
if (!contentRef.value) return
|
||||
contentRef.value.scrollTop = scrollMemory.value[activeTab.value] ?? 0
|
||||
hasTabBarShadow.value = contentRef.value.scrollTop > 0
|
||||
/**
|
||||
* Page width reads as a word rather than a pixel count: readers are choosing how wide
|
||||
* the text column feels, and 400-1600 means nothing without seeing the result.
|
||||
*/
|
||||
const pageWidthLabel = computed(() => {
|
||||
const width = props.state.maxInlineSize
|
||||
if (width <= 640) return t('reader.settings.pageWidthNarrow')
|
||||
if (width <= 1000) return t('reader.settings.pageWidthMedium')
|
||||
if (width <= 1320) return t('reader.settings.pageWidthWide')
|
||||
return t('reader.settings.pageWidthFull')
|
||||
})
|
||||
|
||||
onUnmounted(removePreviewStyles)
|
||||
const columnGapPercent = computed(() => Math.round(props.state.gap * 100))
|
||||
|
||||
function setMode(value: string) {
|
||||
emit('update', { isDark: value === 'dark' })
|
||||
}
|
||||
|
||||
function selectTheme(themeName: string) {
|
||||
emit('update', { themeName })
|
||||
}
|
||||
|
||||
function decreaseTextSize() {
|
||||
emit('update', { fontSize: Math.max(FONT_SIZE_MIN, props.state.fontSize - 1) })
|
||||
}
|
||||
|
||||
function increaseTextSize() {
|
||||
emit('update', { fontSize: Math.min(FONT_SIZE_MAX, props.state.fontSize + 1) })
|
||||
}
|
||||
|
||||
function selectBuiltInFont(font: ReaderBuiltInFontOption) {
|
||||
emit('update', { fontFamily: font.value })
|
||||
}
|
||||
|
||||
function setLineHeight(value: number) {
|
||||
emit('update', { lineHeight: Math.round(value * 10) / 10 })
|
||||
}
|
||||
|
||||
function setPageWidth(value: number) {
|
||||
emit('update', { maxInlineSize: value })
|
||||
}
|
||||
|
||||
function setFlow(value: string) {
|
||||
emit('update', { flow: value as ReaderState['flow'] })
|
||||
}
|
||||
|
||||
function setFixedLayoutSpread(value: string) {
|
||||
emit('update', { fixedLayoutSpread: value as ReaderState['fixedLayoutSpread'] })
|
||||
}
|
||||
|
||||
function decreaseColumns() {
|
||||
emit('update', { maxColumnCount: Math.max(COLUMN_MIN, props.state.maxColumnCount - 1) })
|
||||
}
|
||||
|
||||
function increaseColumns() {
|
||||
emit('update', { maxColumnCount: Math.min(COLUMN_MAX, props.state.maxColumnCount + 1) })
|
||||
}
|
||||
|
||||
function setColumnGap(value: number) {
|
||||
emit('update', { gap: Math.round(value) / 100 })
|
||||
}
|
||||
|
||||
function setJustify(value: boolean) {
|
||||
emit('update', { justify: value })
|
||||
}
|
||||
|
||||
function setHyphenate(value: boolean) {
|
||||
emit('update', { hyphenate: value })
|
||||
}
|
||||
|
||||
function requestReset() {
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
const previewStyleEl = ref<HTMLStyleElement | null>(null)
|
||||
|
||||
function removePreviewStyles() {
|
||||
previewStyleEl.value?.remove()
|
||||
previewStyleEl.value = null
|
||||
}
|
||||
|
||||
/** Lets each custom-font button render in its own typeface, which is the whole point of the preview. */
|
||||
function injectPreviewStyles(css: string) {
|
||||
removePreviewStyles()
|
||||
if (!css) return
|
||||
@@ -96,13 +161,6 @@ function injectPreviewStyles(css: string) {
|
||||
previewStyleEl.value = el
|
||||
}
|
||||
|
||||
function removePreviewStyles() {
|
||||
if (previewStyleEl.value) {
|
||||
previewStyleEl.value.remove()
|
||||
previewStyleEl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.customFonts?.fonts.value, props.customFonts?.serverFonts.value],
|
||||
() => {
|
||||
@@ -111,28 +169,22 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.isFixedLayout,
|
||||
(fixed) => {
|
||||
if (fixed && activeTab.value === 'text') {
|
||||
activeTab.value = 'layout'
|
||||
}
|
||||
},
|
||||
)
|
||||
onUnmounted(removePreviewStyles)
|
||||
|
||||
/** Server fonts first: they are the curated set an administrator chose for everyone. */
|
||||
const customFontSections = computed(() => {
|
||||
const customFonts = props.customFonts
|
||||
if (!customFonts) return []
|
||||
return [
|
||||
{ key: 'server', label: t('reader.settings.text.serverFonts'), families: customFonts.visibleServerFamilies.value },
|
||||
{ key: 'user', label: t('reader.settings.text.yourFonts'), families: customFonts.families.value },
|
||||
{ key: 'server', label: t('reader.settings.fontServer'), families: customFonts.visibleServerFamilies.value },
|
||||
{ key: 'user', label: t('reader.settings.fontYours'), families: customFonts.families.value },
|
||||
].filter((section) => section.families.length > 0)
|
||||
})
|
||||
|
||||
const hasCustomFontSections = computed(() => customFontSections.value.length > 0)
|
||||
|
||||
function selectCustomFont(family: FontFamily) {
|
||||
if (!props.customFonts) return
|
||||
const cssFamilyName = props.customFonts.getCssFamilyForDisplay(family.name, family.scope)
|
||||
const cssFamilyName = props.customFonts?.getCssFamilyForDisplay(family.name, family.scope)
|
||||
if (cssFamilyName) emit('update', { fontFamily: cssFamilyName })
|
||||
}
|
||||
|
||||
@@ -141,329 +193,250 @@ function isCustomFontSelected(family: FontFamily): boolean {
|
||||
return props.customFonts.isFontFamilySelected(family.name, props.state.fontFamily, family.scope)
|
||||
}
|
||||
|
||||
function setFixedLayoutSpreadAuto() {
|
||||
emit('update', { fixedLayoutSpread: 'auto' })
|
||||
}
|
||||
|
||||
function setFixedLayoutSpreadNone() {
|
||||
emit('update', { fixedLayoutSpread: 'none' })
|
||||
}
|
||||
const groupLabelClass = 'mb-2 block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground'
|
||||
const stepperButtonClass =
|
||||
'flex h-10 flex-1 items-center justify-center rounded-lg border border-border font-serif text-foreground transition-colors hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent'
|
||||
const cardBaseClass =
|
||||
'group rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/55 focus-visible:ring-offset-1 focus-visible:ring-offset-card'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="bg-card text-card-foreground flex max-h-[min(80vh,38rem)] flex-col overflow-hidden [&_button:focus-visible]:outline-none [&_button:focus-visible]:ring-2 [&_button:focus-visible]:ring-primary/55 [&_button:focus-visible]:ring-offset-1 [&_button:focus-visible]:ring-offset-card"
|
||||
>
|
||||
<div
|
||||
class="sticky top-0 z-10 border-b border-border bg-card/95 px-3 py-3 backdrop-blur-sm transition-shadow"
|
||||
:class="hasTabBarShadow ? 'shadow-sm' : ''"
|
||||
>
|
||||
<div class="grid grid-cols-3 gap-1 rounded-lg bg-muted/55 p-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex h-8.5 items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
activeTab === tab.id
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="setActiveTab(tab.id)"
|
||||
>
|
||||
<component :is="tab.icon" :size="15" />
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<section class="flex min-h-0 flex-col overflow-hidden bg-card text-card-foreground">
|
||||
<div class="flex shrink-0 items-center gap-2 border-b border-border bg-card px-4 py-2.5 transition-shadow" :class="isScrolled ? 'shadow-sm' : ''">
|
||||
<h2 class="mr-auto text-sm font-semibold">{{ t('reader.settings.title') }}</h2>
|
||||
<ReaderSegmentedControl
|
||||
class="w-[9.75rem] shrink-0"
|
||||
:options="modeOptions"
|
||||
:model-value="currentMode"
|
||||
:aria-label="t('reader.settings.modeLabel')"
|
||||
@update:model-value="setMode"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/55 disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
|
||||
:disabled="!canReset"
|
||||
:title="t('reader.settings.reset')"
|
||||
:aria-label="t('reader.settings.reset')"
|
||||
@click="requestReset"
|
||||
>
|
||||
<RotateCcw :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="contentRef" class="overflow-y-auto p-5.5 space-y-6" @scroll="onContentScroll">
|
||||
<template v-if="activeTab === 'appearance'">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<p class="mb-2 text-[13px] font-medium text-foreground">{{ t('reader.settings.appearance.mode') }}</p>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/55 p-1">
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
!state.isDark
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="emit('update', { isDark: false })"
|
||||
>
|
||||
<Sun :size="15" />
|
||||
<span>{{ t('reader.settings.appearance.light') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
state.isDark
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="emit('update', { isDark: true })"
|
||||
>
|
||||
<Moon :size="15" />
|
||||
<span>{{ t('reader.settings.appearance.dark') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-border/70" />
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-[13px] font-medium text-foreground">{{ t('reader.settings.appearance.colorTheme') }}</p>
|
||||
<div class="grid grid-cols-6 gap-2">
|
||||
<button
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
class="group flex flex-col items-center gap-1.5"
|
||||
@click="emit('update', { themeName: theme.name })"
|
||||
>
|
||||
<div
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full border-2 shadow-sm transition-all"
|
||||
:class="state.themeName === theme.name ? 'scale-110 border-primary' : 'border-transparent group-hover:border-muted-foreground/40'"
|
||||
:style="{ background: state.isDark ? theme.dark.bg : theme.light.bg }"
|
||||
>
|
||||
<div class="h-2.5 w-2.5 rounded-full" :style="{ background: state.isDark ? theme.dark.fg : theme.light.fg }" />
|
||||
</div>
|
||||
<span class="w-full truncate text-center text-[10px] text-muted-foreground">{{ theme.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="contentRef" class="min-h-0 flex-1 overflow-y-auto" @scroll="onContentScroll">
|
||||
<div v-if="!isFixedLayout" class="border-b border-border px-4 py-3.5">
|
||||
<p :class="groupLabelClass">{{ t('reader.settings.textSize') }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="stepperButtonClass"
|
||||
class="text-sm"
|
||||
:disabled="state.fontSize <= FONT_SIZE_MIN"
|
||||
:aria-label="t('reader.settings.textSizeSmaller')"
|
||||
@click="decreaseTextSize"
|
||||
>
|
||||
A
|
||||
</button>
|
||||
<span class="w-16 shrink-0 rounded-lg bg-muted py-2 text-center text-[13px] font-semibold tabular-nums text-foreground">
|
||||
{{ t('reader.settings.pixels', { value: state.fontSize }) }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
:class="stepperButtonClass"
|
||||
class="text-xl"
|
||||
:disabled="state.fontSize >= FONT_SIZE_MAX"
|
||||
:aria-label="t('reader.settings.textSizeLarger')"
|
||||
@click="increaseTextSize"
|
||||
>
|
||||
A
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'text' && !isFixedLayout">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.text.fontSize') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.text.fontSizeRange') }}</p>
|
||||
</div>
|
||||
<div :class="stepperGroupClass">
|
||||
<button :class="stepperButtonClass" @click="step('fontSize', -1, 10, 32)">−</button>
|
||||
<span :class="stepperValueClass">{{ state.fontSize }}px</span>
|
||||
<button :class="stepperButtonClass" @click="step('fontSize', 1, 10, 32)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.text.lineHeight') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.text.lineHeightRange') }}</p>
|
||||
</div>
|
||||
<div :class="stepperGroupClass">
|
||||
<button :class="stepperButtonClass" @click="step('lineHeight', -0.1, 0.8, 3, 1)">−</button>
|
||||
<span :class="stepperValueClass">{{ state.lineHeight.toFixed(1) }}</span>
|
||||
<button :class="stepperButtonClass" @click="step('lineHeight', 0.1, 0.8, 3, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-border/70" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.text.fontFamily') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.text.fontFamilyDescription') }}</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{{ t('reader.settings.text.builtIn') }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="font in BUILTIN_READER_FONT_OPTIONS"
|
||||
:key="String(font.value)"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="
|
||||
state.fontFamily === font.value
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted'
|
||||
"
|
||||
:style="font.value ? { fontFamily: font.value } : {}"
|
||||
@click="emit('update', { fontFamily: font.value })"
|
||||
>
|
||||
{{ font.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-for="section in customFontSections" :key="section.key">
|
||||
<div class="h-px bg-border/70" />
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{{ section.label }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="family in section.families"
|
||||
:key="family.cssFamilyName"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="
|
||||
isCustomFontSelected(family)
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border hover:border-muted-foreground/40 hover:bg-muted'
|
||||
"
|
||||
:style="{ fontFamily: `'${family.cssFamilyName}', sans-serif` }"
|
||||
@click="selectCustomFont(family)"
|
||||
>
|
||||
{{ formatFontFamilyLabel(family.name) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-b border-border px-4 py-3.5">
|
||||
<p :class="groupLabelClass">{{ t('reader.settings.pageColor') }}</p>
|
||||
<div class="grid grid-cols-4 gap-x-2 gap-y-1.5">
|
||||
<button
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
type="button"
|
||||
:class="cardBaseClass"
|
||||
:aria-pressed="state.themeName === theme.name"
|
||||
:aria-label="t(theme.labelKey)"
|
||||
@click="selectTheme(theme.name)"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="relative flex h-9 w-full items-center justify-center overflow-hidden rounded-md font-serif text-[13px] ring-2 ring-offset-2 ring-offset-card transition-all"
|
||||
:class="state.themeName === theme.name ? 'ring-primary' : 'ring-transparent group-hover:ring-border'"
|
||||
:style="{ background: state.isDark ? theme.dark.bg : theme.light.bg, color: state.isDark ? theme.dark.fg : theme.light.fg }"
|
||||
>
|
||||
<span class="absolute inset-x-0 top-0 h-[3px]" :style="{ background: state.isDark ? theme.dark.link : theme.light.link }" />
|
||||
Aa
|
||||
</span>
|
||||
<span
|
||||
class="mt-0.5 block truncate text-center text-[10px] leading-tight"
|
||||
:class="state.themeName === theme.name ? 'text-foreground' : 'text-muted-foreground'"
|
||||
>
|
||||
{{ t(theme.labelKey) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'layout'">
|
||||
<div class="space-y-6">
|
||||
<template v-if="isFixedLayout">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.pageSpreads') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.pageSpreadsDescription') }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/55 p-1">
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
state.fixedLayoutSpread === 'auto'
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="setFixedLayoutSpreadAuto"
|
||||
>
|
||||
<LayoutGrid :size="15" />
|
||||
<span>{{ t('reader.settings.layout.bookDefault') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
state.fixedLayoutSpread === 'none'
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="setFixedLayoutSpreadNone"
|
||||
>
|
||||
<BookOpen :size="15" />
|
||||
<span>{{ t('reader.settings.layout.singlePage') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!isFixedLayout">
|
||||
<div class="border-b border-border px-4 py-3.5">
|
||||
<p :class="groupLabelClass">{{ t('reader.settings.font') }}</p>
|
||||
<p v-if="hasCustomFontSections" class="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{{ t('reader.settings.fontBuiltIn') }}
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="font in BUILTIN_READER_FONT_OPTIONS"
|
||||
:key="String(font.value)"
|
||||
type="button"
|
||||
class="h-10 truncate rounded-lg border px-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/55 focus-visible:ring-offset-1 focus-visible:ring-offset-card"
|
||||
:class="
|
||||
state.fontFamily === font.value
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border text-foreground hover:border-muted-foreground/40 hover:bg-muted'
|
||||
"
|
||||
:style="font.value ? { fontFamily: font.value } : {}"
|
||||
:aria-pressed="state.fontFamily === font.value"
|
||||
@click="selectBuiltInFont(font)"
|
||||
>
|
||||
{{ t(font.labelKey) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.readingFlow') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.readingFlowDescription') }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/55 p-1">
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
state.flow === 'paginated'
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="emit('update', { flow: 'paginated' })"
|
||||
>
|
||||
<BookOpen :size="15" />
|
||||
<span>{{ t('reader.settings.layout.paginated') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex h-[2.125rem] items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors"
|
||||
:class="
|
||||
state.flow === 'scrolled'
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/70'
|
||||
"
|
||||
@click="emit('update', { flow: 'scrolled' })"
|
||||
>
|
||||
<ScrollText :size="15" />
|
||||
<span>{{ t('reader.settings.layout.scrolled') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-border/70" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.textColumns') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.textColumnsRange') }}</p>
|
||||
</div>
|
||||
<div :class="stepperGroupClass">
|
||||
<button :class="stepperButtonClass" @click="step('maxColumnCount', -1, 1, 10)">−</button>
|
||||
<span :class="stepperValueClass">{{ state.maxColumnCount }}</span>
|
||||
<button :class="stepperButtonClass" @click="step('maxColumnCount', 1, 1, 10)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.columnGap') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.columnGapRange') }}</p>
|
||||
</div>
|
||||
<div :class="stepperGroupClass">
|
||||
<button :class="stepperButtonClass" @click="step('gap', -0.01, 0, 0.5, 2)">−</button>
|
||||
<span :class="stepperValueClass">{{ formatGap(state.gap) }}</span>
|
||||
<button :class="stepperButtonClass" @click="step('gap', 0.01, 0, 0.5, 2)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.maxWidth') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.maxWidthRange') }}</p>
|
||||
</div>
|
||||
<div :class="stepperGroupClass">
|
||||
<button :class="stepperButtonClass" @click="step('maxInlineSize', -40, 400, 1600)">−</button>
|
||||
<span :class="stepperValueClass">{{ state.maxInlineSize }}px</span>
|
||||
<button :class="stepperButtonClass" @click="step('maxInlineSize', 40, 400, 1600)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-border/70" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.justifyText') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.justifyTextDescription') }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative h-6 w-11 shrink-0 rounded-full transition-colors"
|
||||
:class="state.justify ? 'bg-primary' : 'bg-muted'"
|
||||
@click="emit('update', { justify: !state.justify })"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 h-4 w-4 rounded-full bg-white shadow-sm transition-transform"
|
||||
:class="state.justify ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1 pr-3">
|
||||
<p class="text-sm font-medium leading-tight">{{ t('reader.settings.layout.hyphenation') }}</p>
|
||||
<p class="text-xs leading-tight text-muted-foreground">{{ t('reader.settings.layout.hyphenationDescription') }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative h-6 w-11 shrink-0 rounded-full transition-colors"
|
||||
:class="state.hyphenate ? 'bg-primary' : 'bg-muted'"
|
||||
@click="emit('update', { hyphenate: !state.hyphenate })"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 h-4 w-4 rounded-full bg-white shadow-sm transition-transform"
|
||||
:class="state.hyphenate ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<template v-for="section in customFontSections" :key="section.key">
|
||||
<p class="mb-1.5 mt-3 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{{ section.label }}</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="family in section.families"
|
||||
:key="family.cssFamilyName"
|
||||
type="button"
|
||||
class="h-10 truncate rounded-lg border px-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/55 focus-visible:ring-offset-1 focus-visible:ring-offset-card"
|
||||
:class="
|
||||
isCustomFontSelected(family)
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border text-foreground hover:border-muted-foreground/40 hover:bg-muted'
|
||||
"
|
||||
:style="{ fontFamily: `'${family.cssFamilyName}', sans-serif` }"
|
||||
:aria-pressed="isCustomFontSelected(family)"
|
||||
@click="selectCustomFont(family)"
|
||||
>
|
||||
{{ formatFontFamilyLabel(family.name) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-border px-4 py-3.5">
|
||||
<ReaderRangeField
|
||||
:model-value="state.lineHeight"
|
||||
:min="0.8"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
:label="t('reader.settings.lineSpacing')"
|
||||
:display-value="state.lineHeight.toFixed(1)"
|
||||
:min-icon="Rows4"
|
||||
:max-icon="Rows2"
|
||||
@update:model-value="setLineHeight"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-border px-4 py-3.5">
|
||||
<ReaderRangeField
|
||||
:model-value="state.maxInlineSize"
|
||||
:min="400"
|
||||
:max="1600"
|
||||
:step="40"
|
||||
:label="t('reader.settings.pageWidth')"
|
||||
:display-value="pageWidthLabel"
|
||||
:min-icon="RectangleVertical"
|
||||
:max-icon="RectangleHorizontal"
|
||||
@update:model-value="setPageWidth"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-border px-4 py-3.5">
|
||||
<p :class="groupLabelClass">{{ t('reader.settings.readingFlow') }}</p>
|
||||
<ReaderSegmentedControl
|
||||
:options="flowOptions"
|
||||
:model-value="state.flow"
|
||||
:aria-label="t('reader.settings.readingFlow')"
|
||||
@update:model-value="setFlow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details class="group/adv">
|
||||
<summary
|
||||
class="flex cursor-pointer list-none items-center gap-2 px-4 py-3 text-[13px] font-semibold text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/55 [&::-webkit-details-marker]:hidden"
|
||||
>
|
||||
{{ t('reader.settings.advanced') }}
|
||||
<ChevronDown :size="14" class="ml-auto transition-transform group-open/adv:rotate-180" />
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 px-4 pb-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-[13px] font-medium text-foreground">{{ t('reader.settings.columns') }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-border text-lg font-light text-foreground transition-colors hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
:disabled="state.maxColumnCount <= COLUMN_MIN"
|
||||
:aria-label="t('reader.settings.columnsFewer')"
|
||||
@click="decreaseColumns"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="w-6 text-center text-[13px] font-semibold tabular-nums text-foreground">{{ state.maxColumnCount }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-border text-lg font-light text-foreground transition-colors hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
:disabled="state.maxColumnCount >= COLUMN_MAX"
|
||||
:aria-label="t('reader.settings.columnsMore')"
|
||||
@click="increaseColumns"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReaderRangeField
|
||||
:model-value="columnGapPercent"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
:label="t('reader.settings.columnGap')"
|
||||
:display-value="t('reader.settings.percent', { value: columnGapPercent })"
|
||||
@update:model-value="setColumnGap"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span :id="justifyLabelId" class="text-[13px] font-medium text-foreground">{{ t('reader.settings.justifyText') }}</span>
|
||||
<ToggleSwitch :model-value="state.justify" :aria-labelledby="justifyLabelId" @update:model-value="setJustify" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span :id="hyphenationLabelId" class="text-[13px] font-medium text-foreground">{{ t('reader.settings.hyphenation') }}</span>
|
||||
<ToggleSwitch :model-value="state.hyphenate" :aria-labelledby="hyphenationLabelId" @update:model-value="setHyphenate" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<div v-else class="px-4 py-3.5">
|
||||
<p :class="groupLabelClass">{{ t('reader.settings.pageSpreads') }}</p>
|
||||
<ReaderSegmentedControl
|
||||
:options="spreadOptions"
|
||||
:model-value="state.fixedLayoutSpread ?? 'auto'"
|
||||
:aria-label="t('reader.settings.pageSpreads')"
|
||||
@update:model-value="setFixedLayoutSpread"
|
||||
/>
|
||||
<p class="mt-2 text-xs leading-snug text-muted-foreground">{{ t('reader.settings.pageSpreadsHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import ReaderHeader from '../ReaderHeader.vue'
|
||||
|
||||
const DropdownMenuStub = defineComponent({
|
||||
name: 'DropdownMenu',
|
||||
const viewport = vi.hoisted(() => ({ isCompact: false }))
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@vueuse/core')>()
|
||||
const { computed } = await import('vue')
|
||||
return { ...actual, useMediaQuery: () => computed(() => viewport.isCompact) }
|
||||
})
|
||||
|
||||
const PopoverStub = defineComponent({
|
||||
name: 'SettingsPopover',
|
||||
emits: ['update:open'],
|
||||
template: '<div><slot /></div>',
|
||||
})
|
||||
|
||||
describe('ReaderHeader', () => {
|
||||
const global = {
|
||||
stubs: {
|
||||
Tooltip: { template: '<div><slot /></div>' },
|
||||
TooltipTrigger: { template: '<div><slot /></div>' },
|
||||
TooltipContent: { template: '<div><slot /></div>' },
|
||||
DropdownMenu: DropdownMenuStub,
|
||||
DropdownMenuTrigger: { template: '<div><slot /></div>' },
|
||||
DropdownMenuContent: { template: '<div><slot /></div>' },
|
||||
},
|
||||
}
|
||||
const SheetStub = defineComponent({
|
||||
name: 'SettingsSheet',
|
||||
emits: ['update:open'],
|
||||
template: '<div><slot /></div>',
|
||||
})
|
||||
|
||||
const global = {
|
||||
stubs: {
|
||||
Tooltip: { template: '<div><slot /></div>' },
|
||||
TooltipTrigger: { template: '<div><slot /></div>' },
|
||||
TooltipContent: { template: '<div><slot /></div>' },
|
||||
Popover: PopoverStub,
|
||||
PopoverTrigger: { template: '<div><slot /></div>' },
|
||||
PopoverContent: { template: '<div><slot /></div>' },
|
||||
Sheet: SheetStub,
|
||||
SheetContent: { template: '<div><slot /></div>' },
|
||||
},
|
||||
}
|
||||
|
||||
function mountHeader(props: Record<string, unknown> = {}) {
|
||||
return mount(ReaderHeader, {
|
||||
props: {
|
||||
chapterTitle: 'Chapter 4',
|
||||
isBookmarked: false,
|
||||
settingsOpen: false,
|
||||
footerMode: 0,
|
||||
...props,
|
||||
},
|
||||
global,
|
||||
})
|
||||
}
|
||||
|
||||
describe('ReaderHeader', () => {
|
||||
it('emits main toolbar actions', async () => {
|
||||
const wrapper = mount(ReaderHeader, {
|
||||
props: {
|
||||
chapterTitle: 'Chapter 4',
|
||||
isBookmarked: false,
|
||||
settingsOpen: false,
|
||||
footerMode: 0,
|
||||
},
|
||||
global,
|
||||
})
|
||||
const wrapper = mountHeader()
|
||||
|
||||
await wrapper.get('button[aria-label="Go back"]').trigger('click')
|
||||
await wrapper.get('button[aria-label="Table of contents"]').trigger('click')
|
||||
@@ -50,18 +71,32 @@ describe('ReaderHeader', () => {
|
||||
expect(wrapper.get('button[aria-label="Enter fullscreen"]').classes()).not.toContain('hidden')
|
||||
})
|
||||
|
||||
it('forwards dropdown open changes', () => {
|
||||
const wrapper = mount(ReaderHeader, {
|
||||
props: {
|
||||
chapterTitle: 'Chapter 4',
|
||||
isBookmarked: true,
|
||||
settingsOpen: true,
|
||||
footerMode: 1,
|
||||
},
|
||||
global,
|
||||
describe('settings container', () => {
|
||||
it('anchors settings to a popover on wide viewports', () => {
|
||||
viewport.isCompact = false
|
||||
const wrapper = mountHeader({ settingsOpen: true })
|
||||
|
||||
expect(wrapper.findComponent(PopoverStub).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(SheetStub).exists()).toBe(false)
|
||||
|
||||
wrapper.findComponent(PopoverStub).vm.$emit('update:open', false)
|
||||
expect(wrapper.emitted('update:settingsOpen')?.[0]).toEqual([false])
|
||||
})
|
||||
|
||||
wrapper.findComponent(DropdownMenuStub).vm.$emit('update:open', false)
|
||||
expect(wrapper.emitted('update:settingsOpen')?.[0]).toEqual([false])
|
||||
it('drops settings into a bottom sheet on compact viewports', async () => {
|
||||
viewport.isCompact = true
|
||||
const wrapper = mountHeader()
|
||||
|
||||
expect(wrapper.findComponent(SheetStub).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(PopoverStub).exists()).toBe(false)
|
||||
|
||||
await wrapper.get('button[aria-label="Reader settings"]').trigger('click')
|
||||
expect(wrapper.emitted('update:settingsOpen')?.[0]).toEqual([true])
|
||||
|
||||
wrapper.findComponent(SheetStub).vm.$emit('update:open', false)
|
||||
expect(wrapper.emitted('update:settingsOpen')?.[1]).toEqual([false])
|
||||
|
||||
viewport.isCompact = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import ReaderSettingsPanel from '../ReaderSettingsPanel.vue'
|
||||
import type { ReaderState } from '../../composables/useReaderState'
|
||||
@@ -22,94 +22,171 @@ function makeState(overrides: Partial<ReaderState> = {}): ReaderState {
|
||||
}
|
||||
}
|
||||
|
||||
function mountPanel(props: Partial<InstanceType<typeof ReaderSettingsPanel>['$props']> = {}) {
|
||||
return mount(ReaderSettingsPanel, {
|
||||
props: { state: makeState(), ...props },
|
||||
})
|
||||
}
|
||||
|
||||
/** Resolves a slider through its <label for>, which also asserts the control is programmatically labelled. */
|
||||
function rangeByLabel(wrapper: VueWrapper, labelText: string) {
|
||||
const label = wrapper.findAll('label').find((candidate) => candidate.text() === labelText)
|
||||
const id = label?.attributes('for')
|
||||
return id ? wrapper.find(`[id="${id}"]`) : undefined
|
||||
}
|
||||
|
||||
/** Resolves a switch through aria-labelledby, so the a11y wiring is covered alongside the behaviour. */
|
||||
function switchByLabel(wrapper: VueWrapper, labelText: string) {
|
||||
return wrapper.findAll('[role="switch"]').find((candidate) => {
|
||||
const labelId = candidate.attributes('aria-labelledby')
|
||||
return labelId ? wrapper.find(`[id="${labelId}"]`).text() === labelText : false
|
||||
})
|
||||
}
|
||||
|
||||
function buttonByAriaLabel(wrapper: VueWrapper, label: string) {
|
||||
return wrapper.find(`button[aria-label="${label}"]`)
|
||||
}
|
||||
|
||||
function buttonByText(wrapper: VueWrapper, text: string) {
|
||||
return wrapper.findAll('button').find((button) => button.text() === text)
|
||||
}
|
||||
|
||||
describe('ReaderSettingsPanel', () => {
|
||||
it('emits incremental updates from text controls', async () => {
|
||||
const wrapper = mount(ReaderSettingsPanel, {
|
||||
props: {
|
||||
state: makeState(),
|
||||
},
|
||||
})
|
||||
it('presents every reflowable setting without tab navigation', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
const textTab = wrapper.findAll('button').find((btn) => btn.text().includes('Text'))
|
||||
await textTab?.trigger('click')
|
||||
expect(wrapper.text()).toContain('Text size')
|
||||
expect(wrapper.text()).toContain('Page color')
|
||||
expect(wrapper.text()).toContain('Font')
|
||||
expect(wrapper.text()).toContain('Line spacing')
|
||||
expect(wrapper.text()).toContain('Page width')
|
||||
expect(wrapper.text()).toContain('Reading flow')
|
||||
expect(wrapper.text()).toContain('Advanced layout')
|
||||
})
|
||||
|
||||
const plusButtons = wrapper.findAll('button').filter((btn) => btn.text() === '+')
|
||||
await plusButtons[0]!.trigger('click')
|
||||
it('steps text size within its bounds', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
await buttonByAriaLabel(wrapper, 'Larger text').trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ fontSize: 17 }])
|
||||
|
||||
await buttonByAriaLabel(wrapper, 'Smaller text').trigger('click')
|
||||
expect(wrapper.emitted('update')?.[1]).toEqual([{ fontSize: 15 }])
|
||||
})
|
||||
|
||||
it('emits appearance and layout toggles', async () => {
|
||||
const wrapper = mount(ReaderSettingsPanel, {
|
||||
props: {
|
||||
state: makeState({ isDark: false, flow: 'paginated' }),
|
||||
},
|
||||
})
|
||||
it('disables the text size steppers at the ends of the range', () => {
|
||||
const atMin = mountPanel({ state: makeState({ fontSize: 10 }) })
|
||||
expect(buttonByAriaLabel(atMin, 'Smaller text').attributes('disabled')).toBeDefined()
|
||||
expect(buttonByAriaLabel(atMin, 'Larger text').attributes('disabled')).toBeUndefined()
|
||||
|
||||
const darkButton = wrapper.findAll('button').find((btn) => btn.text().includes('Dark'))
|
||||
await darkButton?.trigger('click')
|
||||
const atMax = mountPanel({ state: makeState({ fontSize: 32 }) })
|
||||
expect(buttonByAriaLabel(atMax, 'Larger text').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('switches colour mode and marks the active segment', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ isDark: false }) })
|
||||
|
||||
const dark = buttonByText(wrapper, 'Dark')
|
||||
expect(dark?.attributes('aria-pressed')).toBe('false')
|
||||
expect(buttonByText(wrapper, 'Light')?.attributes('aria-pressed')).toBe('true')
|
||||
|
||||
await dark?.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ isDark: true }])
|
||||
|
||||
const layoutTab = wrapper.findAll('button').find((btn) => btn.text().includes('Layout'))
|
||||
await layoutTab?.trigger('click')
|
||||
|
||||
const scrolledButton = wrapper.findAll('button').find((btn) => btn.text().includes('Scrolled'))
|
||||
await scrolledButton?.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('update')?.[1]).toEqual([{ flow: 'scrolled' }])
|
||||
})
|
||||
|
||||
it('emits fixed-layout EPUB spread updates', async () => {
|
||||
const wrapper = mount(ReaderSettingsPanel, {
|
||||
props: {
|
||||
state: makeState({ fixedLayoutSpread: 'auto' }),
|
||||
isFixedLayout: true,
|
||||
},
|
||||
})
|
||||
it('selects a page colour theme', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ themeName: 'default' }) })
|
||||
|
||||
const layoutTab = wrapper.findAll('button').find((btn) => btn.text().includes('Layout'))
|
||||
await layoutTab?.trigger('click')
|
||||
await buttonByAriaLabel(wrapper, 'Sepia').trigger('click')
|
||||
|
||||
const singlePageButton = wrapper.findAll('button').find((btn) => btn.text().includes('Single page'))
|
||||
await singlePageButton?.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ themeName: 'sepia' }])
|
||||
})
|
||||
|
||||
it('selects a built-in font', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
await buttonByText(wrapper, 'Sans-serif')?.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ fontFamily: 'sans-serif' }])
|
||||
|
||||
await buttonByText(wrapper, 'Book default')?.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[1]).toEqual([{ fontFamily: null }])
|
||||
})
|
||||
|
||||
it('emits rounded line spacing from the slider', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
const slider = rangeByLabel(wrapper, 'Line spacing')!
|
||||
await slider.setValue('1.8')
|
||||
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ lineHeight: 1.8 }])
|
||||
})
|
||||
|
||||
it('describes page width in words rather than pixels', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ maxInlineSize: 720 }) })
|
||||
|
||||
const slider = rangeByLabel(wrapper, 'Page width')!
|
||||
expect(slider.attributes('aria-valuetext')).toBe('Medium')
|
||||
|
||||
await slider.setValue('440')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ maxInlineSize: 440 }])
|
||||
|
||||
expect(mountPanel({ state: makeState({ maxInlineSize: 440 }) }).text()).toContain('Narrow')
|
||||
expect(mountPanel({ state: makeState({ maxInlineSize: 1600 }) }).text()).toContain('Full')
|
||||
})
|
||||
|
||||
it('switches reading flow', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ flow: 'paginated' }) })
|
||||
|
||||
await buttonByText(wrapper, 'Scroll')?.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ flow: 'scrolled' }])
|
||||
})
|
||||
|
||||
it('emits advanced layout changes', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ maxColumnCount: 2, gap: 0.05, justify: true, hyphenate: true }) })
|
||||
|
||||
await buttonByAriaLabel(wrapper, 'More columns').trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ maxColumnCount: 3 }])
|
||||
|
||||
await rangeByLabel(wrapper, 'Column gap')!.setValue('12')
|
||||
expect(wrapper.emitted('update')?.[1]).toEqual([{ gap: 0.12 }])
|
||||
|
||||
await switchByLabel(wrapper, 'Justify text')!.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[2]).toEqual([{ justify: false }])
|
||||
|
||||
await switchByLabel(wrapper, 'Hyphenation')!.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[3]).toEqual([{ hyphenate: false }])
|
||||
})
|
||||
|
||||
it('resets only when the book carries overrides', async () => {
|
||||
const untouched = mountPanel({ canReset: false })
|
||||
expect(buttonByAriaLabel(untouched, 'Reset to defaults').attributes('disabled')).toBeDefined()
|
||||
|
||||
const customized = mountPanel({ canReset: true })
|
||||
const reset = buttonByAriaLabel(customized, 'Reset to defaults')
|
||||
expect(reset.attributes('disabled')).toBeUndefined()
|
||||
|
||||
await reset.trigger('click')
|
||||
expect(customized.emitted('reset')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('offers spread choices and hides text controls for fixed-layout books', async () => {
|
||||
const wrapper = mountPanel({ state: makeState({ fixedLayoutSpread: 'auto' }), isFixedLayout: true })
|
||||
|
||||
expect(wrapper.text()).toContain('Page spreads')
|
||||
expect(wrapper.text()).toContain('Page color')
|
||||
expect(wrapper.text()).not.toContain('Text size')
|
||||
expect(wrapper.text()).not.toContain('Reading flow')
|
||||
expect(wrapper.text()).not.toContain('Advanced layout')
|
||||
|
||||
await buttonByText(wrapper, 'Single page')?.trigger('click')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual([{ fixedLayoutSpread: 'none' }])
|
||||
})
|
||||
|
||||
it('hides fixed-layout spread controls for reflowable books', async () => {
|
||||
const wrapper = mount(ReaderSettingsPanel, {
|
||||
props: {
|
||||
state: makeState({ fixedLayoutSpread: 'auto' }),
|
||||
},
|
||||
})
|
||||
it('hides fixed-layout spread controls for reflowable books', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
const layoutTab = wrapper.findAll('button').find((btn) => btn.text().includes('Layout'))
|
||||
await layoutTab?.trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('Reading flow')
|
||||
expect(wrapper.text()).toContain('Text columns')
|
||||
expect(wrapper.text()).not.toContain('Page spreads')
|
||||
expect(wrapper.text()).not.toContain('Single page')
|
||||
})
|
||||
|
||||
it('shows only fixed-layout layout controls for fixed-layout books', async () => {
|
||||
const wrapper = mount(ReaderSettingsPanel, {
|
||||
props: {
|
||||
state: makeState({ fixedLayoutSpread: 'auto', flow: 'scrolled' }),
|
||||
isFixedLayout: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('button').some((btn) => btn.text().includes('Text'))).toBe(false)
|
||||
|
||||
const layoutTab = wrapper.findAll('button').find((btn) => btn.text().includes('Layout'))
|
||||
await layoutTab?.trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('Page spreads')
|
||||
expect(wrapper.text()).toContain('Book default')
|
||||
expect(wrapper.text()).toContain('Single page')
|
||||
expect(wrapper.text()).not.toContain('Reading flow')
|
||||
expect(wrapper.text()).not.toContain('Scrolled')
|
||||
expect(wrapper.text()).not.toContain('Text columns')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface ThemeMode {
|
||||
|
||||
export interface Theme {
|
||||
name: string
|
||||
label: string
|
||||
labelKey: string
|
||||
light: ThemeMode
|
||||
dark: ThemeMode
|
||||
fg?: string
|
||||
@@ -17,79 +17,79 @@ export interface Theme {
|
||||
export const themes: Theme[] = [
|
||||
{
|
||||
name: 'default',
|
||||
label: 'Default',
|
||||
labelKey: 'reader.settings.themes.default',
|
||||
light: { fg: '#000000', bg: '#ffffff', link: '#0066cc' },
|
||||
dark: { fg: '#e0e0e0', bg: '#222222', link: '#77bbee' },
|
||||
},
|
||||
{
|
||||
name: 'gray',
|
||||
label: 'Gray',
|
||||
labelKey: 'reader.settings.themes.gray',
|
||||
light: { fg: '#222222', bg: '#e0e0e0', link: '#4488cc' },
|
||||
dark: { fg: '#c6c6c6', bg: '#444444', link: '#88ccee' },
|
||||
},
|
||||
{
|
||||
name: 'sepia',
|
||||
label: 'Sepia',
|
||||
labelKey: 'reader.settings.themes.sepia',
|
||||
light: { fg: '#5b4636', bg: '#f1e8d0', link: '#008b8b' },
|
||||
dark: { fg: '#ffd595', bg: '#342e25', link: '#48d1cc' },
|
||||
},
|
||||
{
|
||||
name: 'crimson',
|
||||
label: 'Crimson',
|
||||
labelKey: 'reader.settings.themes.crimson',
|
||||
light: { fg: '#2f1f25', bg: '#fdf1f4', link: '#dd0031' },
|
||||
dark: { fg: '#f3dbe2', bg: '#3a252d', link: '#ff5a86' },
|
||||
},
|
||||
{
|
||||
name: 'meadow',
|
||||
label: 'Meadow',
|
||||
labelKey: 'reader.settings.themes.meadow',
|
||||
light: { fg: '#232c16', bg: '#d7dbbd', link: '#177b4d' },
|
||||
dark: { fg: '#d8deba', bg: '#333627', link: '#a6d608' },
|
||||
},
|
||||
{
|
||||
name: 'rosewood',
|
||||
label: 'Rosewood',
|
||||
labelKey: 'reader.settings.themes.rosewood',
|
||||
light: { fg: '#4e1609', bg: '#f0d1d5', link: '#de3838' },
|
||||
dark: { fg: '#e5c4c8', bg: '#462f32', link: '#ff646e' },
|
||||
},
|
||||
{
|
||||
name: 'azure',
|
||||
label: 'Azure',
|
||||
labelKey: 'reader.settings.themes.azure',
|
||||
light: { fg: '#262d48', bg: '#cedef5', link: '#2d53e5' },
|
||||
dark: { fg: '#babee1', bg: '#282e47', link: '#ff646e' },
|
||||
},
|
||||
{
|
||||
name: 'dawnlight',
|
||||
label: 'Dawnlight',
|
||||
labelKey: 'reader.settings.themes.dawnlight',
|
||||
light: { fg: '#586e75', bg: '#fdf6e3', link: '#268bd2' },
|
||||
dark: { fg: '#93a1a1', bg: '#002b36', link: '#268bd2' },
|
||||
},
|
||||
{
|
||||
name: 'ember',
|
||||
label: 'Ember',
|
||||
labelKey: 'reader.settings.themes.ember',
|
||||
light: { fg: '#3c3836', bg: '#fbf1c7', link: '#076678' },
|
||||
dark: { fg: '#ebdbb2', bg: '#282828', link: '#83a598' },
|
||||
},
|
||||
{
|
||||
name: 'aurora',
|
||||
label: 'Aurora',
|
||||
labelKey: 'reader.settings.themes.aurora',
|
||||
light: { fg: '#2e3440', bg: '#eceff4', link: '#5e81ac' },
|
||||
dark: { fg: '#d8dee9', bg: '#2e3440', link: '#88c0d0' },
|
||||
},
|
||||
{
|
||||
name: 'ocean',
|
||||
label: 'Ocean',
|
||||
labelKey: 'reader.settings.themes.ocean',
|
||||
light: { fg: '#0a4d4d', bg: '#e0f7fa', link: '#00838f' },
|
||||
dark: { fg: '#b2dfdb', bg: '#263238', link: '#4dd0e1' },
|
||||
},
|
||||
{
|
||||
name: 'mist',
|
||||
label: 'Mist',
|
||||
labelKey: 'reader.settings.themes.mist',
|
||||
light: { fg: '#4a148c', bg: '#f3e5f5', link: '#7b1fa2' },
|
||||
dark: { fg: '#c7b6dd', bg: '#3a3150', link: '#b39ddb' },
|
||||
},
|
||||
{
|
||||
name: 'amoled',
|
||||
label: 'AMOLED',
|
||||
labelKey: 'reader.settings.themes.amoled',
|
||||
light: { fg: '#000000', bg: '#ffffff', link: '#0066cc' },
|
||||
dark: { fg: '#ffffff', bg: '#000000', link: '#77bbee' },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useId, type Component } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
label: string
|
||||
/** Human-readable current value, shown beside the track and announced in place of the raw number. */
|
||||
displayValue: string
|
||||
minIcon?: Component
|
||||
maxIcon?: Component
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number] }>()
|
||||
|
||||
const inputId = `reader-range-${useId()}`
|
||||
|
||||
const fillPercent = computed(() => {
|
||||
const span = props.max - props.min
|
||||
if (span <= 0) return 0
|
||||
return ((props.modelValue - props.min) / span) * 100
|
||||
})
|
||||
|
||||
function onInput(event: Event) {
|
||||
emit('update:modelValue', Number((event.target as HTMLInputElement).value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label :for="inputId" class="mb-2 block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{{ label }}</label>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<component :is="minIcon" v-if="minIcon" :size="15" class="shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<input
|
||||
:id="inputId"
|
||||
type="range"
|
||||
class="reader-range min-w-0 flex-1"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:value="modelValue"
|
||||
:aria-valuetext="displayValue"
|
||||
:style="{ '--reader-range-fill': `${fillPercent}%` }"
|
||||
@input="onInput"
|
||||
/>
|
||||
<component :is="maxIcon" v-if="maxIcon" :size="15" class="shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<span class="w-14 shrink-0 text-right text-xs font-semibold tabular-nums text-foreground">{{ displayValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reader-range {
|
||||
appearance: none;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(to right, var(--primary) var(--reader-range-fill), var(--border) var(--reader-range-fill));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reader-range:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.reader-range::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--card);
|
||||
border-radius: 999px;
|
||||
background: var(--primary);
|
||||
box-shadow: var(--elevation-sm);
|
||||
}
|
||||
|
||||
.reader-range::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--card);
|
||||
border-radius: 999px;
|
||||
background: var(--primary);
|
||||
box-shadow: var(--elevation-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export interface ReaderSegmentedOption {
|
||||
value: string
|
||||
label: string
|
||||
icon?: Component
|
||||
}
|
||||
|
||||
// aria-label falls through to the root group from the call site rather than being a prop,
|
||||
// so it stays a plain attribute instead of shadowing the native one.
|
||||
defineProps<{
|
||||
options: ReaderSegmentedOption[]
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
function select(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-1 rounded-lg bg-muted/60 p-1" role="group">
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:aria-pressed="modelValue === option.value"
|
||||
class="flex h-8.5 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-md px-2 text-[13px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/55 focus-visible:ring-offset-1 focus-visible:ring-offset-card"
|
||||
:class="
|
||||
modelValue === option.value
|
||||
? 'bg-background text-foreground shadow-sm ring-1 ring-border'
|
||||
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
|
||||
"
|
||||
@click="select(option.value)"
|
||||
>
|
||||
<component :is="option.icon" v-if="option.icon" :size="14" class="shrink-0" />
|
||||
<span class="truncate">{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,16 @@
|
||||
export interface ReaderBuiltInFontOption {
|
||||
value: string | null
|
||||
label: string
|
||||
labelKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliberately short: the real decision is serif vs sans, and named families duplicate what
|
||||
* the generic keywords already resolve to while falling back unpredictably per platform.
|
||||
* Readers who want a specific face upload it as a user or server font.
|
||||
*/
|
||||
export const BUILTIN_READER_FONT_OPTIONS: ReaderBuiltInFontOption[] = [
|
||||
{ value: null, label: 'Book default' },
|
||||
{ value: 'serif', label: 'Serif' },
|
||||
{ value: 'sans-serif', label: 'Sans-serif' },
|
||||
{ value: 'monospace', label: 'Monospace' },
|
||||
{ value: 'Georgia, serif', label: 'Georgia' },
|
||||
{ value: 'Palatino Linotype, Palatino, Book Antiqua, serif', label: 'Palatino' },
|
||||
{ value: null, labelKey: 'reader.settings.fonts.bookDefault' },
|
||||
{ value: 'serif', labelKey: 'reader.settings.fonts.serif' },
|
||||
{ value: 'sans-serif', labelKey: 'reader.settings.fonts.sansSerif' },
|
||||
{ value: 'monospace', labelKey: 'reader.settings.fonts.monospace' },
|
||||
]
|
||||
|
||||
@@ -226,36 +226,41 @@ function setFixedLayoutSpreadNone() {
|
||||
</div>
|
||||
<!-- Theme swatches -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-3">
|
||||
<button v-for="t in themes" :key="t.name" class="flex flex-col items-center gap-1.5 group" @click="update({ themeName: t.name })">
|
||||
<button
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
class="flex flex-col items-center gap-1.5 group"
|
||||
@click="update({ themeName: theme.name })"
|
||||
>
|
||||
<div
|
||||
class="relative w-full aspect-[4/3] rounded-lg overflow-hidden transition-all ring-2 ring-offset-2 ring-offset-card"
|
||||
:class="effective.themeName === t.name ? 'ring-primary' : 'ring-transparent group-hover:ring-border'"
|
||||
:style="{ background: effective.isDark ? t.dark.bg : t.light.bg }"
|
||||
:class="effective.themeName === theme.name ? 'ring-primary' : 'ring-transparent group-hover:ring-border'"
|
||||
:style="{ background: effective.isDark ? theme.dark.bg : theme.light.bg }"
|
||||
>
|
||||
<!-- Top accent strip -->
|
||||
<div class="absolute top-0 left-0 right-0 h-[3px]" :style="{ background: effective.isDark ? t.dark.link : t.light.link }" />
|
||||
<div class="absolute top-0 left-0 right-0 h-[3px]" :style="{ background: effective.isDark ? theme.dark.link : theme.light.link }" />
|
||||
<!-- Title line -->
|
||||
<div
|
||||
class="absolute top-[10px] left-[8px] right-[12px] h-[3px] rounded-full"
|
||||
:style="{ background: effective.isDark ? t.dark.fg : t.light.fg, opacity: 0.85 }"
|
||||
:style="{ background: effective.isDark ? theme.dark.fg : theme.light.fg, opacity: 0.85 }"
|
||||
/>
|
||||
<!-- Body text lines -->
|
||||
<div
|
||||
class="absolute top-[18px] left-[8px] right-[8px] h-[2px] rounded-full"
|
||||
:style="{ background: effective.isDark ? t.dark.fg : t.light.fg, opacity: 0.35 }"
|
||||
:style="{ background: effective.isDark ? theme.dark.fg : theme.light.fg, opacity: 0.35 }"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-[23px] left-[8px] right-[16px] h-[2px] rounded-full"
|
||||
:style="{ background: effective.isDark ? t.dark.fg : t.light.fg, opacity: 0.35 }"
|
||||
:style="{ background: effective.isDark ? theme.dark.fg : theme.light.fg, opacity: 0.35 }"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-[28px] left-[8px] right-[10px] h-[2px] rounded-full"
|
||||
:style="{ background: effective.isDark ? t.dark.fg : t.light.fg, opacity: 0.35 }"
|
||||
:style="{ background: effective.isDark ? theme.dark.fg : theme.light.fg, opacity: 0.35 }"
|
||||
/>
|
||||
<!-- Link dot -->
|
||||
<div
|
||||
class="absolute bottom-[7px] left-[8px] h-[2px] w-[14px] rounded-full opacity-80"
|
||||
:style="{ background: effective.isDark ? t.dark.link : t.light.link }"
|
||||
:style="{ background: effective.isDark ? theme.dark.link : theme.light.link }"
|
||||
/>
|
||||
<!-- Selected checkmark -->
|
||||
<Transition
|
||||
@@ -265,7 +270,7 @@ function setFixedLayoutSpreadNone() {
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="effective.themeName === t.name"
|
||||
v-if="effective.themeName === theme.name"
|
||||
class="absolute bottom-[5px] right-[6px] w-4 h-4 rounded-full bg-primary flex items-center justify-center shadow"
|
||||
>
|
||||
<Check :size="9" class="text-primary-foreground" :stroke-width="3" />
|
||||
@@ -274,9 +279,9 @@ function setFixedLayoutSpreadNone() {
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium transition-colors leading-none text-center"
|
||||
:class="effective.themeName === t.name ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'"
|
||||
:class="effective.themeName === theme.name ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'"
|
||||
>
|
||||
{{ t.label }}
|
||||
{{ t(theme.labelKey) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -299,9 +304,9 @@ function setFixedLayoutSpreadNone() {
|
||||
@change="update({ fontFamily: ($event.target as HTMLSelectElement).value || null })"
|
||||
>
|
||||
<optgroup :label="t('settings.reader.ebook.builtInFonts')">
|
||||
<option v-for="f in BUILTIN_READER_FONT_OPTIONS" :key="String(f.value)" :value="f.value ?? ''">{{ f.label }}</option>
|
||||
<option v-for="f in BUILTIN_READER_FONT_OPTIONS" :key="String(f.value)" :value="f.value ?? ''">{{ t(f.labelKey) }}</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="serverFontOptions.length > 0" :label="t('reader.settings.text.serverFonts')">
|
||||
<optgroup v-if="serverFontOptions.length > 0" :label="t('reader.settings.fontServer')">
|
||||
<option v-for="f in serverFontOptions" :key="f.id" :value="f.id">{{ f.label }}</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="customFontOptions.length > 0" :label="t('settings.reader.ebook.yourFonts')">
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Nastavení",
|
||||
"ariaLabel": "Nastavení čtečky",
|
||||
"tabs": {
|
||||
"appearance": "Vzhled",
|
||||
"text": "Text",
|
||||
"layout": "Rozložení"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Režim",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Světlý",
|
||||
"dark": "Tmavý",
|
||||
"colorTheme": "Barevný motiv"
|
||||
"dark": "Tmavý"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Rozsah: 10-32px",
|
||||
"lineHeight": "Výška čáry",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Rodina písma",
|
||||
"fontFamilyDescription": "Vyberte vestavěná nebo nahraná písma pro text těla.",
|
||||
"builtIn": "Vestavěné",
|
||||
"yourFonts": "Vaše písma",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Vestavěné",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Vaše písma",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Měření průtoku",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Rozložení stránky",
|
||||
"pageSpreadsDescription": "Vyberte si, jak tyto EPUB stránky založené na obrázcích.",
|
||||
"pageSpreads": "Rozložení stránky",
|
||||
"pageSpreadsHint": "Vyberte si, jak tyto EPUB stránky založené na obrázcích.",
|
||||
"spread": {
|
||||
"bookDefault": "Výchozí rezervace",
|
||||
"singlePage": "Jedna stránka",
|
||||
"readingFlow": "Měření průtoku",
|
||||
"readingFlowDescription": "Přepínání mezi stránkováním a spojitým rolováním.",
|
||||
"paginated": "Stránkováno",
|
||||
"scrolled": "Posunout",
|
||||
"textColumns": "Sloupce textu",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Mezera sloupce",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Zarovnat text",
|
||||
"justifyTextDescription": "Povolit zarovnání odstavce na celou šířku.",
|
||||
"hyphenation": "Hyfenace",
|
||||
"hyphenationDescription": "Povolit automatickou pomlčku slova."
|
||||
"singlePage": "Jedna stránka"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Mezera sloupce",
|
||||
"justifyText": "Zarovnat text",
|
||||
"hyphenation": "Hyfenace",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Indstillinger",
|
||||
"ariaLabel": "Læser indstillinger",
|
||||
"tabs": {
|
||||
"appearance": "Udseende",
|
||||
"text": "Tekst",
|
||||
"layout": "Layout"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Tilstand",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Lys",
|
||||
"dark": "Mørk",
|
||||
"colorTheme": "Farve tema"
|
||||
"dark": "Mørk"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Interval: 10-32px",
|
||||
"lineHeight": "Linje højde",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Skrifttype familie",
|
||||
"fontFamilyDescription": "Vælg indbyggede eller uploadede skrifttyper til brødtekst.",
|
||||
"builtIn": "Indbygget",
|
||||
"yourFonts": "Dine skrifttyper",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Indbygget",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Dine skrifttyper",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Læser flow",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Sidespreads",
|
||||
"pageSpreadsDescription": "Vælg hvordan dette billede-baserede EPUB par sider.",
|
||||
"pageSpreads": "Sidespreads",
|
||||
"pageSpreadsHint": "Vælg hvordan dette billede-baserede EPUB par sider.",
|
||||
"spread": {
|
||||
"bookDefault": "Bogs standard",
|
||||
"singlePage": "Enkelt side",
|
||||
"readingFlow": "Læser flow",
|
||||
"readingFlowDescription": "Skift mellem sidekædet og kontinuerlig rulning.",
|
||||
"paginated": "Pagineret",
|
||||
"scrolled": "Rullet",
|
||||
"textColumns": "Tekst kolonner",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Kolonne mellemrum",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Justér tekst",
|
||||
"justifyTextDescription": "Aktivér justering af afsnit i fuld bredde.",
|
||||
"hyphenation": "Hyphenation",
|
||||
"hyphenationDescription": "Aktiver automatisk ordskift bindeling."
|
||||
"singlePage": "Enkelt side"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Kolonne mellemrum",
|
||||
"justifyText": "Justér tekst",
|
||||
"hyphenation": "Hyphenation",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"ariaLabel": "Lesereinstellungen",
|
||||
"tabs": {
|
||||
"appearance": "Erscheinungsbild",
|
||||
"text": "Text",
|
||||
"layout": "Layout"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Modus",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"colorTheme": "Farbschema"
|
||||
"dark": "Dunkel"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Schriftgröße",
|
||||
"fontSizeRange": "Bereich: 10-32px",
|
||||
"lineHeight": "Zeilenhöhe",
|
||||
"lineHeightRange": "Bereich: 0.8-3.0",
|
||||
"fontFamily": "Schriftart",
|
||||
"fontFamilyDescription": "Wählen Sie integrierte oder hochgeladene Schriftarten für den Fließtext.",
|
||||
"builtIn": "Integriert",
|
||||
"yourFonts": "Ihre Schriftarten",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Integriert",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Ihre Schriftarten",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Lesefluss",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Doppelseiten",
|
||||
"pageSpreadsDescription": "Wählen Sie, wie dieses bildbasierte EPUB Seiten paart.",
|
||||
"pageSpreads": "Doppelseiten",
|
||||
"pageSpreadsHint": "Wählen Sie, wie dieses bildbasierte EPUB Seiten paart.",
|
||||
"spread": {
|
||||
"bookDefault": "Buchvorgabe",
|
||||
"singlePage": "Einzelseite",
|
||||
"readingFlow": "Lesefluss",
|
||||
"readingFlowDescription": "Zwischen seitenweisem und fortlaufendem Scrollen wechseln.",
|
||||
"paginated": "Seitenweise",
|
||||
"scrolled": "Scrollen",
|
||||
"textColumns": "Textspalten",
|
||||
"textColumnsRange": "Bereich: 1-10",
|
||||
"columnGap": "Spaltenabstand",
|
||||
"columnGapRange": "Bereich: 0-50%",
|
||||
"maxWidth": "Maximale Breite",
|
||||
"maxWidthRange": "Bereich: 400-1600",
|
||||
"justifyText": "Text ausrichten",
|
||||
"justifyTextDescription": "Blocksatz für Absätze aktivieren.",
|
||||
"hyphenation": "Silbentrennung",
|
||||
"hyphenationDescription": "Automatische Silbentrennung aktivieren."
|
||||
"singlePage": "Einzelseite"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Spaltenabstand",
|
||||
"justifyText": "Text ausrichten",
|
||||
"hyphenation": "Silbentrennung",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"ariaLabel": "Reader settings",
|
||||
"tabs": {
|
||||
"appearance": "Appearance",
|
||||
"text": "Text",
|
||||
"layout": "Layout"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Mode",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"colorTheme": "Color theme"
|
||||
"dark": "Dark"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Range: 10-32px",
|
||||
"lineHeight": "Line height",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Font family",
|
||||
"fontFamilyDescription": "Choose built-in or uploaded fonts for body text.",
|
||||
"builtIn": "Built-in",
|
||||
"yourFonts": "Your fonts",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Built-in",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Your fonts",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Reading flow",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Page spreads",
|
||||
"pageSpreadsDescription": "Choose how this image-based EPUB pairs pages.",
|
||||
"pageSpreads": "Page spreads",
|
||||
"pageSpreadsHint": "Choose how this image-based EPUB pairs pages.",
|
||||
"spread": {
|
||||
"bookDefault": "Book default",
|
||||
"singlePage": "Single page",
|
||||
"readingFlow": "Reading flow",
|
||||
"readingFlowDescription": "Switch between paged and continuous scrolling.",
|
||||
"paginated": "Paginated",
|
||||
"scrolled": "Scrolled",
|
||||
"textColumns": "Text columns",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Column gap",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Justify text",
|
||||
"justifyTextDescription": "Enable full-width paragraph alignment.",
|
||||
"hyphenation": "Hyphenation",
|
||||
"hyphenationDescription": "Enable automatic word-break hyphenation."
|
||||
"singlePage": "Single page"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Column gap",
|
||||
"justifyText": "Justify text",
|
||||
"hyphenation": "Hyphenation",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"ariaLabel": "Configuración del lector",
|
||||
"tabs": {
|
||||
"appearance": "Apariencia",
|
||||
"text": "Texto",
|
||||
"layout": "Diseño"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Modo",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Luz",
|
||||
"dark": "oscuro",
|
||||
"colorTheme": "Tema de color"
|
||||
"dark": "oscuro"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Tamaño de fuente",
|
||||
"fontSizeRange": "Rango: 10-32px",
|
||||
"lineHeight": "altura de la línea",
|
||||
"lineHeightRange": "Rango: 0,8-3,0",
|
||||
"fontFamily": "Familia de fuentes",
|
||||
"fontFamilyDescription": "Elija fuentes integradas o cargadas para el cuerpo del texto.",
|
||||
"builtIn": "Incorporado",
|
||||
"yourFonts": "Tus fuentes",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Incorporado",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Tus fuentes",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Flujo de lectura",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Extensiones de página",
|
||||
"pageSpreadsDescription": "Elija cómo este EPUB basado en imágenes empareja páginas.",
|
||||
"pageSpreads": "Extensiones de página",
|
||||
"pageSpreadsHint": "Elija cómo este EPUB basado en imágenes empareja páginas.",
|
||||
"spread": {
|
||||
"bookDefault": "Reservar por defecto",
|
||||
"singlePage": "Una sola página",
|
||||
"readingFlow": "Flujo de lectura",
|
||||
"readingFlowDescription": "Cambie entre desplazamiento paginado y continuo.",
|
||||
"paginated": "paginado",
|
||||
"scrolled": "Desplazado",
|
||||
"textColumns": "Columnas de texto",
|
||||
"textColumnsRange": "Rango: 1-10",
|
||||
"columnGap": "Espacio de columna",
|
||||
"columnGapRange": "Rango: 0-50%",
|
||||
"maxWidth": "Ancho máximo",
|
||||
"maxWidthRange": "Rango: 400-1600",
|
||||
"justifyText": "Justificar texto",
|
||||
"justifyTextDescription": "Habilite la alineación de párrafos de ancho completo.",
|
||||
"hyphenation": "Separación de sílabas",
|
||||
"hyphenationDescription": "Habilite la separación de palabras automática."
|
||||
"singlePage": "Una sola página"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Espacio de columna",
|
||||
"justifyText": "Justificar texto",
|
||||
"hyphenation": "Separación de sílabas",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Asetukset",
|
||||
"ariaLabel": "Lukijan asetukset",
|
||||
"tabs": {
|
||||
"appearance": "Ulkoasu",
|
||||
"text": "Teksti",
|
||||
"layout": "Asettelu"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Tila",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Vaalea",
|
||||
"dark": "Tumma",
|
||||
"colorTheme": "Teeman väri"
|
||||
"dark": "Tumma"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Alue: 10-32px",
|
||||
"lineHeight": "Viivan korkeus",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Kirjasinperhe",
|
||||
"fontFamilyDescription": "Valitse sisäänrakennetut tai ladatut kirjasimet vartalotekstille.",
|
||||
"builtIn": "Sisäänrakennettu",
|
||||
"yourFonts": "Kirjasimesi",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Sisäänrakennettu",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Kirjasimesi",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Lukemisen virta",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Sivun levitykset",
|
||||
"pageSpreadsDescription": "Valitse, miten tämä kuvapohjainen EPUB paria sivut.",
|
||||
"pageSpreads": "Sivun levitykset",
|
||||
"pageSpreadsHint": "Valitse, miten tämä kuvapohjainen EPUB paria sivut.",
|
||||
"spread": {
|
||||
"bookDefault": "Varaa oletus",
|
||||
"singlePage": "Yksi sivu",
|
||||
"readingFlow": "Lukemisen virta",
|
||||
"readingFlowDescription": "Vaihda sivutun ja jatkuvan vierityksen välillä.",
|
||||
"paginated": "Sivutettu",
|
||||
"scrolled": "Vieritetty",
|
||||
"textColumns": "Tekstin sarakkeet",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Sarakkeen väli",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Tasaa teksti",
|
||||
"justifyTextDescription": "Ota koko leveyden kohdistus käyttöön.",
|
||||
"hyphenation": "Hyfenaatio",
|
||||
"hyphenationDescription": "Ota käyttöön automaattinen sananmurron vältys."
|
||||
"singlePage": "Yksi sivu"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Sarakkeen väli",
|
||||
"justifyText": "Tasaa teksti",
|
||||
"hyphenation": "Hyfenaatio",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"ariaLabel": "Paramètres du lecteur",
|
||||
"tabs": {
|
||||
"appearance": "Apparence",
|
||||
"text": "Texte",
|
||||
"layout": "Disposition"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Mode",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Lumière",
|
||||
"dark": "Sombre",
|
||||
"colorTheme": "Thème de couleur"
|
||||
"dark": "Sombre"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Taille de la police",
|
||||
"fontSizeRange": "Plage : 10-32 px",
|
||||
"lineHeight": "Hauteur de ligne",
|
||||
"lineHeightRange": "Plage : 0,8-3,0",
|
||||
"fontFamily": "Famille de polices",
|
||||
"fontFamilyDescription": "Choisissez des polices intégrées ou téléchargées pour le corps du texte.",
|
||||
"builtIn": "Intégré",
|
||||
"yourFonts": "Vos polices",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Intégré",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Vos polices",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Flux de lecture",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Pages doubles",
|
||||
"pageSpreadsDescription": "Choisissez comment ce EPUB basé sur des images associe les pages.",
|
||||
"pageSpreads": "Pages doubles",
|
||||
"pageSpreadsHint": "Choisissez comment ce EPUB basé sur des images associe les pages.",
|
||||
"spread": {
|
||||
"bookDefault": "Livre par défaut",
|
||||
"singlePage": "Page unique",
|
||||
"readingFlow": "Flux de lecture",
|
||||
"readingFlowDescription": "Basculez entre le défilement paginé et continu.",
|
||||
"paginated": "Paginé",
|
||||
"scrolled": "Défilé",
|
||||
"textColumns": "Colonnes de texte",
|
||||
"textColumnsRange": "Plage : 1-10",
|
||||
"columnGap": "Écart de colonne",
|
||||
"columnGapRange": "Plage : 0-50 %",
|
||||
"maxWidth": "Largeur maximale",
|
||||
"maxWidthRange": "Plage : 400-1600",
|
||||
"justifyText": "Justifier le texte",
|
||||
"justifyTextDescription": "Activer l’alignement des paragraphes sur toute la largeur.",
|
||||
"hyphenation": "Césure",
|
||||
"hyphenationDescription": "Activez la césure automatique des mots."
|
||||
"singlePage": "Page unique"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Écart de colonne",
|
||||
"justifyText": "Justifier le texte",
|
||||
"hyphenation": "Césure",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"ariaLabel": "Impostazioni del lettore",
|
||||
"tabs": {
|
||||
"appearance": "Aspetto",
|
||||
"text": "Testo",
|
||||
"layout": "Disposizione"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Modalità",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Luce",
|
||||
"dark": "Buio",
|
||||
"colorTheme": "Tema del colore"
|
||||
"dark": "Buio"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Dimensione del carattere",
|
||||
"fontSizeRange": "Intervallo: 10-32px",
|
||||
"lineHeight": "Altezza della linea",
|
||||
"lineHeightRange": "Intervallo: 0,8-3,0",
|
||||
"fontFamily": "Famiglia di caratteri",
|
||||
"fontFamilyDescription": "Scegli i caratteri incorporati o caricati per il corpo del testo.",
|
||||
"builtIn": "Integrato",
|
||||
"yourFonts": "I tuoi caratteri",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Integrato",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "I tuoi caratteri",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Flusso di lettura",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "La pagina si allarga",
|
||||
"pageSpreadsDescription": "Scegli come questo EPUB basato su immagini accoppia le pagine.",
|
||||
"pageSpreads": "La pagina si allarga",
|
||||
"pageSpreadsHint": "Scegli come questo EPUB basato su immagini accoppia le pagine.",
|
||||
"spread": {
|
||||
"bookDefault": "Prenota predefinito",
|
||||
"singlePage": "Pagina singola",
|
||||
"readingFlow": "Flusso di lettura",
|
||||
"readingFlowDescription": "Passa dallo scorrimento pagina a quello continuo.",
|
||||
"paginated": "Paginato",
|
||||
"scrolled": "Scorrito",
|
||||
"textColumns": "Colonne di testo",
|
||||
"textColumnsRange": "Intervallo: 1-10",
|
||||
"columnGap": "Spazio tra le colonne",
|
||||
"columnGapRange": "Intervallo: 0-50%",
|
||||
"maxWidth": "Larghezza massima",
|
||||
"maxWidthRange": "Intervallo: 400-1600",
|
||||
"justifyText": "Giustificare il testo",
|
||||
"justifyTextDescription": "Abilita l'allineamento del paragrafo a larghezza intera.",
|
||||
"hyphenation": "Sillabazione",
|
||||
"hyphenationDescription": "Abilita la sillabazione automatica delle interruzioni di parola."
|
||||
"singlePage": "Pagina singola"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Spazio tra le colonne",
|
||||
"justifyText": "Giustificare il testo",
|
||||
"hyphenation": "Sillabazione",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"ariaLabel": "Lezerinstellingen",
|
||||
"tabs": {
|
||||
"appearance": "Weergave",
|
||||
"text": "Tekst",
|
||||
"layout": "Indeling"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Modus",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Licht",
|
||||
"dark": "Donker",
|
||||
"colorTheme": "Kleurthema"
|
||||
"dark": "Donker"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Lettergrootte",
|
||||
"fontSizeRange": "Bereik: 10-32px",
|
||||
"lineHeight": "Regelhoogte",
|
||||
"lineHeightRange": "Bereik: 0,8-3,0",
|
||||
"fontFamily": "Lettertype",
|
||||
"fontFamilyDescription": "Kies ingebouwde of geüploade lettertypen voor de tekst.",
|
||||
"builtIn": "Ingebouwd",
|
||||
"yourFonts": "Jouw lettertypen",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Ingebouwd",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Jouw lettertypen",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Leesstroom",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Paginaspreads",
|
||||
"pageSpreadsDescription": "Kies hoe deze op afbeeldingen gebaseerde EPUB pagina's koppelt.",
|
||||
"pageSpreads": "Paginaspreads",
|
||||
"pageSpreadsHint": "Kies hoe deze op afbeeldingen gebaseerde EPUB pagina's koppelt.",
|
||||
"spread": {
|
||||
"bookDefault": "Standaard van boek",
|
||||
"singlePage": "Enkele pagina",
|
||||
"readingFlow": "Leesstroom",
|
||||
"readingFlowDescription": "Wissel tussen gepagineerd en doorlopend scrollen.",
|
||||
"paginated": "Gepagineerd",
|
||||
"scrolled": "Scrollend",
|
||||
"textColumns": "Tekstkolommen",
|
||||
"textColumnsRange": "Bereik: 1-10",
|
||||
"columnGap": "Kolomtussenruimte",
|
||||
"columnGapRange": "Bereik: 0-50%",
|
||||
"maxWidth": "Maximale breedte",
|
||||
"maxWidthRange": "Bereik: 400-1600",
|
||||
"justifyText": "Tekst uitvullen",
|
||||
"justifyTextDescription": "Uitlijning over de volledige breedte inschakelen.",
|
||||
"hyphenation": "Woordafbreking",
|
||||
"hyphenationDescription": "Automatische woordafbreking inschakelen."
|
||||
"singlePage": "Enkele pagina"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Kolomtussenruimte",
|
||||
"justifyText": "Tekst uitvullen",
|
||||
"hyphenation": "Woordafbreking",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Ustawienia",
|
||||
"ariaLabel": "Ustawienia czytnika",
|
||||
"tabs": {
|
||||
"appearance": "Wygląd",
|
||||
"text": "Tekst",
|
||||
"layout": "Układ"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Tryb",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Światło",
|
||||
"dark": "Ciemny",
|
||||
"colorTheme": "Motyw koloru"
|
||||
"dark": "Ciemny"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Rozmiar czcionki",
|
||||
"fontSizeRange": "Zakres: 10-32px",
|
||||
"lineHeight": "Wysokość linii",
|
||||
"lineHeightRange": "Zakres: 0,8-3,0",
|
||||
"fontFamily": "Rodzina czcionek",
|
||||
"fontFamilyDescription": "Wybierz wbudowane lub przesłane czcionki dla tekstu podstawowego.",
|
||||
"builtIn": "Wbudowany",
|
||||
"yourFonts": "Twoje czcionki",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Wbudowany",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Twoje czcionki",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Czytanie",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Strona się rozprzestrzenia",
|
||||
"pageSpreadsDescription": "Wybierz sposób, w jaki ten oparty na obrazie EPUB łączy strony w pary.",
|
||||
"pageSpreads": "Strona się rozprzestrzenia",
|
||||
"pageSpreadsHint": "Wybierz sposób, w jaki ten oparty na obrazie EPUB łączy strony w pary.",
|
||||
"spread": {
|
||||
"bookDefault": "Książka domyślna",
|
||||
"singlePage": "Pojedyncza strona",
|
||||
"readingFlow": "Czytanie",
|
||||
"readingFlowDescription": "Przełączanie pomiędzy przewijaniem stronicowanym i ciągłym.",
|
||||
"paginated": "Paginowane",
|
||||
"scrolled": "Przewinięte",
|
||||
"textColumns": "Kolumny tekstowe",
|
||||
"textColumnsRange": "Zakres: 1-10",
|
||||
"columnGap": "Szczelina kolumny",
|
||||
"columnGapRange": "Zakres: 0-50%",
|
||||
"maxWidth": "Maksymalna szerokość",
|
||||
"maxWidthRange": "Zakres: 400-1600",
|
||||
"justifyText": "Justuj tekst",
|
||||
"justifyTextDescription": "Włącz wyrównanie akapitu na całej szerokości.",
|
||||
"hyphenation": "Dzielenie wyrazów",
|
||||
"hyphenationDescription": "Włącz automatyczne dzielenie wyrazów."
|
||||
"singlePage": "Pojedyncza strona"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Szczelina kolumny",
|
||||
"justifyText": "Justuj tekst",
|
||||
"hyphenation": "Dzielenie wyrazów",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"ariaLabel": "Configurações do Leitor",
|
||||
"tabs": {
|
||||
"appearance": "Aparência",
|
||||
"text": "Texto",
|
||||
"layout": "Layout"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Modo",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Claro",
|
||||
"dark": "Escuro",
|
||||
"colorTheme": "Tema de cor"
|
||||
"dark": "Escuro"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Tamanho da fonte",
|
||||
"fontSizeRange": "Intervalo: 10-32px",
|
||||
"lineHeight": "Altura da linha",
|
||||
"lineHeightRange": "Intervalo: 0.8-3.0",
|
||||
"fontFamily": "Família de fontes",
|
||||
"fontFamilyDescription": "Escolha as fontes internas ou importadas para o corpo do texto.",
|
||||
"builtIn": "Internas",
|
||||
"yourFonts": "Suas fontes",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Internas",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Suas fontes",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Fluxo de leitura",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Disposição das páginas",
|
||||
"pageSpreadsDescription": "Escolha como esse EPUB baseado em imagem emparelha as páginas.",
|
||||
"pageSpreads": "Disposição das páginas",
|
||||
"pageSpreadsHint": "Escolha como esse EPUB baseado em imagem emparelha as páginas.",
|
||||
"spread": {
|
||||
"bookDefault": "Padrão do livro",
|
||||
"singlePage": "Página única",
|
||||
"readingFlow": "Fluxo de leitura",
|
||||
"readingFlowDescription": "Alterne entre rolagem contínua e páginas separadas.",
|
||||
"paginated": "Paginado",
|
||||
"scrolled": "Rolar",
|
||||
"textColumns": "Colunas de texto",
|
||||
"textColumnsRange": "Intervalo: 1-10",
|
||||
"columnGap": "Espaçamento das colunas",
|
||||
"columnGapRange": "Intervalo: 0-50%",
|
||||
"maxWidth": "Largura máxima",
|
||||
"maxWidthRange": "Intervalo: 400-1600",
|
||||
"justifyText": "Justificar texto",
|
||||
"justifyTextDescription": "Habilitar alinhamento de parágrafos para a largura completa.",
|
||||
"hyphenation": "Hifenização",
|
||||
"hyphenationDescription": "Habilitar hifenização automática na quebra de linha."
|
||||
"singlePage": "Página única"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Espaçamento das colunas",
|
||||
"justifyText": "Justificar texto",
|
||||
"hyphenation": "Hifenização",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
"ariaLabel": "Настройки чтения",
|
||||
"tabs": {
|
||||
"appearance": "Внешний вид",
|
||||
"text": "Текст",
|
||||
"layout": "Макет"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Режим",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
"colorTheme": "Цветовая схема"
|
||||
"dark": "Тёмная"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Диапазон: 10-32px",
|
||||
"lineHeight": "Высота линии",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Семейство шрифтов",
|
||||
"fontFamilyDescription": "Выберите встроенные или загруженные шрифты для текста тела.",
|
||||
"builtIn": "Встроенный",
|
||||
"yourFonts": "Ваши шрифты",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Встроенный",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Ваши шрифты",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Чтение потока",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Спреды страниц",
|
||||
"pageSpreadsDescription": "Выберите, как это пары страниц EPUB на основе образа.",
|
||||
"pageSpreads": "Спреды страниц",
|
||||
"pageSpreadsHint": "Выберите, как это пары страниц EPUB на основе образа.",
|
||||
"spread": {
|
||||
"bookDefault": "Книга по умолчанию",
|
||||
"singlePage": "Одна страница",
|
||||
"readingFlow": "Чтение потока",
|
||||
"readingFlowDescription": "Переключение между нагруженной и непрерывной прокруткой.",
|
||||
"paginated": "Популярно",
|
||||
"scrolled": "Прокрутка",
|
||||
"textColumns": "Текстовые колонки",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Разрыв столбцов",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Выровнять текст по ширине",
|
||||
"justifyTextDescription": "Включить выравнивание параграфов всей ширины.",
|
||||
"hyphenation": "Перенос",
|
||||
"hyphenationDescription": "Включить автоматическое перенос слов."
|
||||
"singlePage": "Одна страница"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Разрыв столбцов",
|
||||
"justifyText": "Выровнять текст по ширине",
|
||||
"hyphenation": "Перенос",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Nastavitve",
|
||||
"ariaLabel": "Nastavitve bralnika",
|
||||
"tabs": {
|
||||
"appearance": "Videz",
|
||||
"text": "Besedilo",
|
||||
"layout": "Postavitev"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Način",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Svetlo",
|
||||
"dark": "Temno",
|
||||
"colorTheme": "Barvna tema"
|
||||
"dark": "Temno"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Velikost pisave",
|
||||
"fontSizeRange": "Obseg: 10-32px",
|
||||
"lineHeight": "Višina vrstice",
|
||||
"lineHeightRange": "Obseg: 0.8-3.0",
|
||||
"fontFamily": "Družina pisav",
|
||||
"fontFamilyDescription": "Izberite vgrajene ali naložene pisave za besedilo.",
|
||||
"builtIn": "Vgrajeno",
|
||||
"yourFonts": "Vaše pisave",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Vgrajeno",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Vaše pisave",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Potek branja",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Razpon strani",
|
||||
"pageSpreadsDescription": "Izberite, kako ta na slikah temelječ EPUB združuje strani.",
|
||||
"pageSpreads": "Razpon strani",
|
||||
"pageSpreadsHint": "Izberite, kako ta na slikah temelječ EPUB združuje strani.",
|
||||
"spread": {
|
||||
"bookDefault": "Privzeto za knjigo",
|
||||
"singlePage": "Ena stran",
|
||||
"readingFlow": "Potek branja",
|
||||
"readingFlowDescription": "Preklapljajte med branjem po straneh in neprekinjenim drsenjem.",
|
||||
"paginated": "Po straneh",
|
||||
"scrolled": "Drsenje",
|
||||
"textColumns": "Stolpci besedila",
|
||||
"textColumnsRange": "Obseg: 1-10",
|
||||
"columnGap": "Razmik med stolpci",
|
||||
"columnGapRange": "Obseg: 0-50%",
|
||||
"maxWidth": "Največja širina",
|
||||
"maxWidthRange": "Obseg: 400-1600",
|
||||
"justifyText": "Poravnaj besedilo",
|
||||
"justifyTextDescription": "Omogoči poravnavo odstavkov na polno širino.",
|
||||
"hyphenation": "Deljenje besed",
|
||||
"hyphenationDescription": "Omogoči samodejno deljenje besed."
|
||||
"singlePage": "Ena stran"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Razmik med stolpci",
|
||||
"justifyText": "Poravnaj besedilo",
|
||||
"hyphenation": "Deljenje besed",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
"ariaLabel": "Inställningar för läsare",
|
||||
"tabs": {
|
||||
"appearance": "Utseende",
|
||||
"text": "Text",
|
||||
"layout": "Layout"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Läge",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Ljus",
|
||||
"dark": "Mörk",
|
||||
"colorTheme": "Färg tema"
|
||||
"dark": "Mörk"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Räckvidd: 10-32px",
|
||||
"lineHeight": "Linje höjd",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Typsnitt familj",
|
||||
"fontFamilyDescription": "Välj inbyggda eller uppladdade teckensnitt för kroppstext.",
|
||||
"builtIn": "Inbyggd",
|
||||
"yourFonts": "Dina typsnitt",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Inbyggd",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Dina typsnitt",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Läser flöde",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Sid spreads",
|
||||
"pageSpreadsDescription": "Välj hur denna bildbaserade EPUB parar sidor.",
|
||||
"pageSpreads": "Sid spreads",
|
||||
"pageSpreadsHint": "Välj hur denna bildbaserade EPUB parar sidor.",
|
||||
"spread": {
|
||||
"bookDefault": "Bokens standard",
|
||||
"singlePage": "Enda sidan",
|
||||
"readingFlow": "Läser flöde",
|
||||
"readingFlowDescription": "Växla mellan sidrullning och kontinuerlig rullning.",
|
||||
"paginated": "Sidnumrerad",
|
||||
"scrolled": "Rullad",
|
||||
"textColumns": "Text kolumner",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Kolumn mellanrum",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "Justera text",
|
||||
"justifyTextDescription": "Aktivera justering av punkt i full bredd.",
|
||||
"hyphenation": "Hyfenering",
|
||||
"hyphenationDescription": "Aktivera automatisk word-break bindestreck."
|
||||
"singlePage": "Enda sidan"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Kolumn mellanrum",
|
||||
"justifyText": "Justera text",
|
||||
"hyphenation": "Hyfenering",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "Налаштування",
|
||||
"ariaLabel": "Параметри читання",
|
||||
"tabs": {
|
||||
"appearance": "Зовнішній вигляд",
|
||||
"text": "Текст",
|
||||
"layout": "Макет"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "Режим",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "Світла",
|
||||
"dark": "Темна",
|
||||
"colorTheme": "Кольорова тема"
|
||||
"dark": "Темна"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "Діапазон: 10-32px",
|
||||
"lineHeight": "Висота рядка",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "Сімейство шрифтів",
|
||||
"fontFamilyDescription": "Виберіть вбудовані або завантажені шрифти для тексту тексту.",
|
||||
"builtIn": "Вбудована тема",
|
||||
"yourFonts": "Ваші шрифти",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "Вбудована тема",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "Ваші шрифти",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "Потік читання",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "Розгортання сторінки",
|
||||
"pageSpreadsDescription": "Виберіть, як відображаються сторінки пар на основі зображень.",
|
||||
"pageSpreads": "Розгортання сторінки",
|
||||
"pageSpreadsHint": "Виберіть, як відображаються сторінки пар на основі зображень.",
|
||||
"spread": {
|
||||
"bookDefault": "Книга за замовчуванням",
|
||||
"singlePage": "Одна сторінка",
|
||||
"readingFlow": "Потік читання",
|
||||
"readingFlowDescription": "Перемикання між вивантаженими та безперервною прокручуванням.",
|
||||
"paginated": "Розбивка на сторінки",
|
||||
"scrolled": "Прокрутити",
|
||||
"textColumns": "Текстові стовпці",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "Розрив стовпчика",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "По ширині тексту",
|
||||
"justifyTextDescription": "Увімкнути вирівнювання по повній ширині абзац.",
|
||||
"hyphenation": "Дефікація",
|
||||
"hyphenationDescription": "Enable automatic word-break hyphenation."
|
||||
"singlePage": "Одна сторінка"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "Розрив стовпчика",
|
||||
"justifyText": "По ширині тексту",
|
||||
"hyphenation": "Дефікація",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
+56
-37
@@ -5952,47 +5952,66 @@
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"ariaLabel": "阅读器设置",
|
||||
"tabs": {
|
||||
"appearance": "外观",
|
||||
"text": "文本",
|
||||
"layout": "布局"
|
||||
},
|
||||
"appearance": {
|
||||
"mode": "模式",
|
||||
"reset": "Reset to defaults",
|
||||
"modeLabel": "Color mode",
|
||||
"mode": {
|
||||
"light": "亮色的",
|
||||
"dark": "深色",
|
||||
"colorTheme": "颜色主题"
|
||||
"dark": "深色"
|
||||
},
|
||||
"text": {
|
||||
"fontSize": "Font size",
|
||||
"fontSizeRange": "范围: 10-32px",
|
||||
"lineHeight": "行高度",
|
||||
"lineHeightRange": "Range: 0.8-3.0",
|
||||
"fontFamily": "字体类",
|
||||
"fontFamilyDescription": "选择内置字体或上传字体。",
|
||||
"builtIn": "内置",
|
||||
"yourFonts": "您的字体",
|
||||
"serverFonts": "Server fonts"
|
||||
"textSize": "Text size",
|
||||
"textSizeSmaller": "Smaller text",
|
||||
"textSizeLarger": "Larger text",
|
||||
"pixels": "{value} px",
|
||||
"percent": "{value}%",
|
||||
"pageColor": "Page color",
|
||||
"font": "Font",
|
||||
"fontBuiltIn": "内置",
|
||||
"fontServer": "Server fonts",
|
||||
"fontYours": "您的字体",
|
||||
"lineSpacing": "Line spacing",
|
||||
"pageWidth": "Page width",
|
||||
"pageWidthNarrow": "Narrow",
|
||||
"pageWidthMedium": "Medium",
|
||||
"pageWidthWide": "Wide",
|
||||
"pageWidthFull": "Full",
|
||||
"readingFlow": "读取流程",
|
||||
"flow": {
|
||||
"paginated": "Pages",
|
||||
"scrolled": "Scroll"
|
||||
},
|
||||
"layout": {
|
||||
"pageSpreads": "页面扩展",
|
||||
"pageSpreadsDescription": "选择这个基于图像的 EPUB 对应页面的方式。",
|
||||
"pageSpreads": "页面扩展",
|
||||
"pageSpreadsHint": "选择这个基于图像的 EPUB 对应页面的方式。",
|
||||
"spread": {
|
||||
"bookDefault": "预约默认",
|
||||
"singlePage": "单页",
|
||||
"readingFlow": "读取流程",
|
||||
"readingFlowDescription": "在分页和连续滚动之间切换。",
|
||||
"paginated": "分页",
|
||||
"scrolled": "已滚动",
|
||||
"textColumns": "文本列",
|
||||
"textColumnsRange": "Range: 1-10",
|
||||
"columnGap": "列间距",
|
||||
"columnGapRange": "Range: 0-50%",
|
||||
"maxWidth": "Max width",
|
||||
"maxWidthRange": "Range: 400-1600",
|
||||
"justifyText": "对齐文本",
|
||||
"justifyTextDescription": "启用全宽段对齐。",
|
||||
"hyphenation": "低温化",
|
||||
"hyphenationDescription": "启用自动断字连字。"
|
||||
"singlePage": "单页"
|
||||
},
|
||||
"advanced": "Advanced layout",
|
||||
"columns": "Columns",
|
||||
"columnsFewer": "Fewer columns",
|
||||
"columnsMore": "More columns",
|
||||
"columnGap": "列间距",
|
||||
"justifyText": "对齐文本",
|
||||
"hyphenation": "低温化",
|
||||
"themes": {
|
||||
"default": "Default",
|
||||
"gray": "Gray",
|
||||
"sepia": "Sepia",
|
||||
"crimson": "Crimson",
|
||||
"meadow": "Meadow",
|
||||
"rosewood": "Rosewood",
|
||||
"azure": "Azure",
|
||||
"dawnlight": "Dawnlight",
|
||||
"ember": "Ember",
|
||||
"aurora": "Aurora",
|
||||
"ocean": "Ocean",
|
||||
"mist": "Mist",
|
||||
"amoled": "AMOLED"
|
||||
},
|
||||
"fonts": {
|
||||
"bookDefault": "Book default",
|
||||
"serif": "Serif",
|
||||
"sansSerif": "Sans-serif",
|
||||
"monospace": "Monospace"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
Reference in New Issue
Block a user