feat(webui): epubreader

Closes: #221
This commit is contained in:
Gauthier Roebroeck 2023-11-28 12:28:39 +08:00
parent a7252f8429
commit 3d69e19fd6
29 changed files with 5678 additions and 333 deletions

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"lint": "vue-cli-service lint --mode production"
},
"dependencies": {
"@d-i-t-a/reader": "https://github.com/gotson/R2D2BC.git#fork",
"@saekitominaga/isbn-verify": "^2.0.1",
"axios": "^1.5.0",
"chart.js": "^2.9.4",

View file

@ -0,0 +1,56 @@
<template>
<v-list v-if="toc"
expand
>
<template v-for="(t, i) in toc">
<v-list-group v-if="t.children"
:key="i"
no-action
>
<template v-slot:activator>
<toc-list-item :item="t" @goto="goto" class="ps-0"/>
</template>
<template v-for="(child, i) in t.children">
<toc-list-item :key="i"
:item="child"
@goto="goto"
:class="`ms-${(child.level - 1) * 4}`"
/>
</template>
</v-list-group>
<!-- Single item -->
<template v-else>
<toc-list-item :key="i" :item="t" @goto="goto"/>
</template>
</template>
</v-list>
</template>
<script lang="ts">
import Vue, {PropType} from 'vue'
import {TocEntry} from '@/types/epub'
import TocListItem from '@/components/TocListItem.vue'
export default Vue.extend({
name: 'TocList',
components: {TocListItem},
data: () => ({}),
props: {
toc: {
type: Array as PropType<TocEntry>[],
required: false,
},
},
computed: {},
methods: {
goto(element: TocEntry) {
this.$emit('goto', element)
},
},
})
</script>
<style>
</style>

View file

@ -0,0 +1,46 @@
<template>
<v-list-item link>
<v-list-item-content>
<v-list-item-title
@click.stop="goto(item)"
:title="item.title"
style="width: 100px"
:class="item.current ? 'primary--text' : ''"
:style="item.href ? 'cursor: pointer' : undefined"
>
<v-badge
:value="!!item.current"
color="primary"
dot
left
offset-y="0"
offset-x="-5"
/>
{{ item.title }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<script lang="ts">
import Vue, {PropType} from 'vue'
import {TocEntry} from '@/types/epub'
export default Vue.extend({
name: 'TocListItem',
props: {
item: {
type: Object as PropType<TocEntry>,
required: true,
},
},
methods: {
goto(element: TocEntry) {
this.$emit('goto', element)
},
},
})
</script>
<style>
</style>

View file

@ -0,0 +1,21 @@
<template>
<svg style="width:24px;height:24px" viewBox="0 0 24 24" class="custom-icon">
<path
d="M 4 3.5 L 4 8.1152344 L 1.5 8.1152344 L 5 11.615234 L 8.5 8.1152344 L 6 8.1152344 L 6 3.5 L 4 3.5 z M 10 5 L 10 7 L 22 7 L 22 5 L 10 5 z M 9.9980469 9.0410156 L 9.9980469 11.041016 L 21.998047 11.041016 L 21.998047 9.0410156 L 9.9980469 9.0410156 z M 5 12.390625 L 1.5 15.890625 L 4 15.890625 L 4 20.5 L 6 20.5 L 6 15.890625 L 8.5 15.890625 L 5 12.390625 z M 10 13 L 10 15 L 22 15 L 22 13 L 10 13 z M 10 17 L 10 19 L 22 19 L 22 17 L 10 17 z "/>
<!-- account -->
</svg>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'IconFormatLineSpacingDown',
})
</script>
<style scoped>
.custom-icon {
fill: currentColor
}
</style>

View file

@ -1,19 +1,29 @@
import {BookFormat} from '@/types/komga-books'
import {lowerCase} from 'lodash'
export function getBookFormatFromMediaType (mediaType: string): BookFormat {
export function getBookFormatFromMediaType(mediaType: string): BookFormat {
switch (mediaType) {
case 'application/x-rar-compressed':
case 'application/x-rar-compressed; version=4':
return { type: 'CBR', color: '#03A9F4' }
return {type: 'CBR', color: '#03A9F4'}
case 'application/zip':
return { type: 'CBZ', color: '#4CAF50' }
return {type: 'CBZ', color: '#4CAF50'}
case 'application/pdf':
return { type: 'PDF', color: '#FF5722' }
return {type: 'PDF', color: '#FF5722'}
case 'application/epub+zip':
return { type: 'EPUB', color: '#ff5ab1' }
return {type: 'EPUB', color: '#ff5ab1'}
case 'application/x-rar-compressed; version=5':
return { type: 'RAR5', color: '#000000' }
return {type: 'RAR5', color: '#000000'}
default:
return { type: mediaType, color: '#000000' }
return {type: mediaType, color: '#000000'}
}
}
export function getBookReadRouteFromMediaProfile(mediaProfile: string): string {
switch (lowerCase(mediaProfile)) {
case 'epub':
return 'read-epub'
default:
return 'read-book'
}
}

View file

@ -0,0 +1,55 @@
import {Shortcut} from '@/types/shortcuts'
export const shortcutsD2Reader = [
new Shortcut('epubreader.shortcuts.previous',
() => {
}, 'ArrowLeft', '←'),
new Shortcut('epubreader.shortcuts.next',
() => {
}, 'ArrowRight', '→'),
new Shortcut('epubreader.shortcuts.previous',
() => {
}, 'CTRL+Space', 'CTRL + SPACE'),
new Shortcut('epubreader.shortcuts.next',
() => {
}, 'Space', 'SPACE'),
]
export const epubShortcutsSettings = [
new Shortcut('epubreader.shortcuts.scroll',
(ctx: any) => ctx.changeLayout(true)
, 'v'),
new Shortcut('epubreader.shortcuts.cycle_pagination',
(ctx: any) => ctx.cyclePagination()
, 'p'),
new Shortcut('epubreader.shortcuts.cycle_viewing_theme',
(ctx: any) => ctx.cycleViewingTheme()
, 'a'),
new Shortcut('epubreader.shortcuts.font_size_increase',
(ctx: any) => ctx.changeFontSize(true)
, '+'),
new Shortcut('epubreader.shortcuts.font_size_decrease',
(ctx: any) => ctx.changeFontSize(false)
, '-'),
new Shortcut('bookreader.shortcuts.fullscreen',
(ctx: any) => ctx.switchFullscreen()
, 'f'),
]
export const epubShortcutsMenus = [
new Shortcut('bookreader.shortcuts.show_hide_toolbars',
(ctx: any) => ctx.toggleToolbars()
, 'm'),
new Shortcut('bookreader.shortcuts.show_hide_settings',
(ctx: any) => ctx.toggleSettings()
, 's'),
new Shortcut('epubreader.shortcuts.show_hide_toc',
(ctx: any) => ctx.toggleTableOfContents()
, 't'),
new Shortcut('bookreader.shortcuts.show_hide_help',
(ctx: any) => ctx.toggleHelp()
, 'h'),
new Shortcut('bookreader.shortcuts.close',
(ctx: any) => ctx.closeDialog()
, 'Escape'),
]

View file

@ -0,0 +1,24 @@
import {TocEntry} from '@/types/epub'
export function flattenToc(toc: TocEntry[], maxLevel: number = 0, currentLevel: number = 0, active?: string): TocEntry[] {
const r: TocEntry[] = []
for (const item of toc) {
const flat = Object.assign({}, item, {level: currentLevel}) as TocEntry
if (flat.href === active) flat.current = true
const children: TocEntry[] = []
if (item.children) {
children.push(...flattenToc(item.children, maxLevel, currentLevel + 1, active))
}
if (currentLevel >= maxLevel) {
delete flat.children
r.push(flat)
r.push(...children)
} else {
flat.children = children.length > 0 ? children : undefined
r.push(flat)
}
}
return r
}

View file

@ -38,6 +38,10 @@ export function bookPageThumbnailUrl(bookId: string, page: number): string {
return `${urls.originNoSlash}/api/v1/books/${bookId}/pages/${page}/thumbnail`
}
export function bookManifestUrl(bookId: string): string {
return `${urls.originNoSlash}/api/v1/books/${bookId}/manifest`
}
export function seriesFileUrl(seriesId: string): string {
return `${urls.originNoSlash}/api/v1/series/${seriesId}/file`
}

View file

@ -636,6 +636,18 @@
"HARDLINK": "Hardlink/Copy Files",
"MOVE": "Move Files"
},
"epubreader": {
"appearances": {
"day": "Day",
"night": "Night",
"sepia": "Sepia"
},
"column_count": {
"auto": "Auto",
"one": "One",
"two": "Two"
}
},
"historical_event_type": {
"BookConverted": "Book converted",
"BookFileDeleted": "Book file deleted",
@ -688,6 +700,29 @@
"XLARGE": "X-Large (1200px)"
}
},
"epubreader": {
"settings": {
"column_count": "Column count",
"layout": "Layout",
"layout_paginated": "Paginated",
"layout_scroll": "Scroll",
"page_margins": "Page margins",
"viewing_theme": "Viewing theme"
},
"shortcuts": {
"cycle_pagination": "Cycle column count",
"cycle_viewing_theme": "Cycle viewing theme",
"font_size_decrease": "Decrease font size",
"font_size_increase": "Increase font size",
"menus": "Menus",
"next": "Forward",
"previous": "Back",
"reader_navigation": "Reader Navigation",
"scroll": "Change layout to scroll",
"settings": "Settings",
"show_hide_toc": "Show/hide table of contents"
}
},
"error_codes": {
"ERR_1000": "File could not be accessed during analysis",
"ERR_1001": "Media type is not supported",

View file

@ -20,6 +20,7 @@ export const persistedModule: Module<any, any> = {
animations: true,
background: '',
},
epubreader: {},
browsingPageSize: undefined as unknown as number,
collection: {
filter: {},
@ -105,6 +106,9 @@ export const persistedModule: Module<any, any> = {
setWebreaderBackground(state, val) {
state.webreader.background = val
},
setEpubreaderSettings(state, val) {
state.epubreader = val
},
setBrowsingPageSize(state, val) {
state.browsingPageSize = val
},

View file

@ -6,6 +6,7 @@ import colors from 'vuetify/lib/util/colors'
import {Touch} from 'vuetify/lib/directives'
import i18n from '@/i18n'
import IconFormatLineSpacingDown from '@/components/icons/IconFormatLineSpacingDown.vue'
Vue.use(Vuetify, {
directives: {
@ -16,6 +17,11 @@ Vue.use(Vuetify, {
export default new Vuetify({
icons: {
iconfont: 'mdi',
values: {
formatLineSpacingDown: {
component: IconFormatLineSpacingDown,
},
},
},
lang: {

View file

@ -263,6 +263,12 @@ const router = new Router({
component: () => import(/* webpackChunkName: "read-book" */ './views/BookReader.vue'),
props: (route) => ({bookId: route.params.bookId}),
},
{
path: '/book/:bookId/read-epub',
name: 'read-epub',
component: () => import(/* webpackChunkName: "read-epub" */ './views/EpubReader.vue'),
props: (route) => ({bookId: route.params.bookId}),
},
{
path: '*',
name: 'notfound',
@ -281,7 +287,7 @@ const router = new Router({
})
router.beforeEach((to, from, next) => {
if (!['read-book', 'browse-book', 'browse-series'].includes(<string>to.name)) {
if (!['read-book', 'read-epub', 'browse-book', 'browse-series'].includes(<string>to.name)) {
document.title = 'Komga'
}

View file

@ -0,0 +1,125 @@
/*
* Copyright 2018-2020 DITA (AM Consulting LLC)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Developed on behalf of: DITA
* Licensed to: CAST under one or more contributor license agreements.
*/
.d2-popover {
position: fixed;
z-index: 10;
top: 0;
display: inline-block;
box-sizing: border-box;
width: 80%;
background: #fafafa;
opacity: 0;
border-radius: 0.5em;
border: 1px solid #c3c3c3;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);
line-height: 0;
-webkit-transition-property: opacity, -webkit-transform;
transition-property: opacity, transform;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
left: 50%;
transform: translateX(-50%) !important;
}
.d2-popover.is-active {
-webkit-transform: scale(1) translateZ(0);
transform: scale(1) translateZ(0);
opacity: 0.97;
}
.d2-popover.is-scrollable:after {
content: '';
position: absolute;
bottom: 0.3375em;
left: 0.3375em;
z-index: 14;
display: block;
height: 0.78125em;
width: 0.625em;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTJweCIgaGVpZ2h0PSIxNXB4IiB2aWV3Qm94PSIwIDAgMTIgMTUiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pbllNaW4iPgogICAgPGcgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IkFycm93IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxLjAwMDAwMCwgMS4wMDAwMDApIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPgogICAgICAgICAgICA8cGF0aCBkPSJNNSwwIEw1LDExLjUiIGlkPSJMaW5lIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLjUsNy41IEw1LjAyNzY5Mjc5LDEyLjAyNzY5MjgiIGlkPSJMaW5lIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik00LjUsNy41IEw5LjAyNzY5Mjc5LDEyLjAyNzY5MjgiIGlkPSJMaW5lLTIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcuMDAwMDAwLCAxMC4wMDAwMDApIHNjYWxlKC0xLCAxKSB0cmFuc2xhdGUoLTcuMDAwMDAwLCAtMTAuMDAwMDAwKSAiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=");
background-size: cover;
opacity: 0.1;
transition-properties: opacity;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
.d2-popover.is-scrollable .d2-d2-popover__wrapper:before, .d2-d2-popover.is-scrollable .d2-d2-popover__wrapper:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
z-index: 12;
left: 0;
}
.d2-d2-popover.is-scrollable .d2-d2-popover__wrapper:before {
top: -1px;
height: 1.1em;
border-radius: 0.5em 0.5em 0 0;
background-image: -webkit-linear-gradient(top, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
background-image: linear-gradient(to bottom, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
}
.d2-popover.is-scrollable .d2-d2-popover__wrapper:after {
bottom: -1px;
height: 1.2em;
border-radius: 0 0 0.5em 0.5em;
background-image: -webkit-linear-gradient(bottom, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
background-image: linear-gradient(to top, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
}
.d2-popover.is-scrollable ::-webkit-scrollbar {
display: none;
}
.d2-popover.is-fully-scrolled:after, .d2-popover.is-fully-scrolled:before {
opacity: 0;
-webkit-transition-delay: 0;
transition-delay: 0;
}
.d2-popover-wrapper {
position: relative;
z-index: 14;
width: 100%;
height: 100%;
display: inline-block;
box-sizing: inherit;
overflow: hidden;
margin: 0;
background-color: #fafafa;
border-radius: 0.5em;
line-height: 0;
/*max-width: 353px;*/
}
.d2-popover-content {
position: relative;
width: 100%;
height: 100%;
z-index: 8;
display: inline-block;
max-height: 90vh;
padding: 1.1em 1.3em 1.2em;
box-sizing: inherit;
overflow: auto;
-webkit-overflow-scrolling: touch;
background: #fafafa;
border-radius: 0.5em;
-webkit-font-smoothing: subpixel-antialiased;
line-height: normal;
}

View file

@ -0,0 +1,121 @@
/*
* Copyright 2018-2020 DITA (AM Consulting LLC)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Developed on behalf of: DITA
* Licensed to: CAST under one or more contributor license agreements.
*/
.d2-popup {
position: fixed;
z-index: 10;
top: 0;
display: inline-block;
box-sizing: border-box;
max-width: 90%;
background: #fafafa;
opacity: 0;
border-radius: 0.5em;
border: 1px solid #c3c3c3;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);
line-height: 0;
-webkit-transition-property: opacity, -webkit-transform;
transition-property: opacity, transform;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
left: 50%;
transform: translateX(-50%) !important;
}
.d2-popup.is-active {
-webkit-transform: scale(1) translateZ(0);
transform: scale(1) translateZ(0);
opacity: 0.97;
}
.d2-popup.is-scrollable:after {
content: '';
position: absolute;
bottom: 0.3375em;
left: 0.3375em;
z-index: 14;
display: block;
height: 0.78125em;
width: 0.625em;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTJweCIgaGVpZ2h0PSIxNXB4IiB2aWV3Qm94PSIwIDAgMTIgMTUiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pbllNaW4iPgogICAgPGcgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IkFycm93IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxLjAwMDAwMCwgMS4wMDAwMDApIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPgogICAgICAgICAgICA8cGF0aCBkPSJNNSwwIEw1LDExLjUiIGlkPSJMaW5lIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLjUsNy41IEw1LjAyNzY5Mjc5LDEyLjAyNzY5MjgiIGlkPSJMaW5lIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik00LjUsNy41IEw5LjAyNzY5Mjc5LDEyLjAyNzY5MjgiIGlkPSJMaW5lLTIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcuMDAwMDAwLCAxMC4wMDAwMDApIHNjYWxlKC0xLCAxKSB0cmFuc2xhdGUoLTcuMDAwMDAwLCAtMTAuMDAwMDAwKSAiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=");
background-size: cover;
opacity: 0.1;
transition-properties: opacity;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
}
.d2-popup.is-scrollable .d2-popup__wrapper:before, .d2-popup.is-scrollable .d2-popup__wrapper:after {
content: '';
position: absolute;
width: 100%;
z-index: 12;
left: 0;
}
.d2-popup.is-scrollable .d2-popup__wrapper:before {
top: -1px;
height: 1.1em;
border-radius: 0.5em 0.5em 0 0;
background-image: -webkit-linear-gradient(top, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
background-image: linear-gradient(to bottom, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
}
.d2-popup.is-scrollable .d2-popup__wrapper:after {
bottom: -1px;
height: 1.2em;
border-radius: 0 0 0.5em 0.5em;
background-image: -webkit-linear-gradient(bottom, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
background-image: linear-gradient(to top, #fafafa 50%, rgba(250, 250, 250, 0) 100%);
}
.d2-popup.is-scrollable ::-webkit-scrollbar {
display: none;
}
.d2-popup.is-fully-scrolled:after, .d2-popup.is-fully-scrolled:before {
opacity: 0;
-webkit-transition-delay: 0;
transition-delay: 0;
}
.d2-popup-wrapper {
position: relative;
z-index: 14;
width: 22em;
display: inline-block;
box-sizing: inherit;
overflow: hidden;
margin: 0;
background-color: #fafafa;
border-radius: 0.5em;
/*line-height: 1;*/
max-width: 353px;
}
.d2-popup-content {
position: relative;
z-index: 8;
display: inline-block;
max-height: 15em;
padding: 1.1em 1.3em 1.2em;
box-sizing: inherit;
overflow: auto;
-webkit-overflow-scrolling: touch;
background: #fafafa;
border-radius: 0.5em;
-webkit-font-smoothing: subpixel-antialiased;
line-height: normal !important;
}

View file

@ -0,0 +1,261 @@
/*
* Copyright 2018-2020 DITA (AM Consulting LLC)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Developed on behalf of: DITA
* Licensed to: CAST under one or more contributor license agreements.
*/
:root {
--RS__highlightColor: rgba(255, 255, 0, 0.5);
--RS__highlightMixBlendMode: multiply;
--RS__highlightHoverColor: rgba(255, 255, 0, 0.75);
--RS__underlineBorderColor: rgba(255, 255, 0, 1);
--RS__underlineHoverColor: rgba(255, 255, 0, 0.1);
--RS__definitionsColor: rgba(255, 111, 111, 0.5);
--RS__definitionsMixBlendMode: multiply;
--RS__definitionsHoverColor: rgba(255, 111, 111, 0.75);
--RS__definitionsUnderlineBorderColor: rgba(255, 111, 111, 1);
--RS__definitionsUnderlineHoverColor: rgba(255, 111, 111, 0.1);
--USER__maxMediaHeight: 95vh;
}
.R2_CLASS_HIGHLIGHT_AREA[data-marker="0"] {
background-color: var(--RS__highlightColor) !important;
mix-blend-mode: var(--RS__highlightMixBlendMode) !important;
z-index: 1;
}
.R2_CLASS_HIGHLIGHT_AREA[data-marker="0"]:hover,
.R2_CLASS_HIGHLIGHT_AREA[data-marker="0"].hover {
background-color: var(--RS__highlightHoverColor) !important;
}
.R2_CLASS_HIGHLIGHT_AREA[data-marker="1"] {
border-bottom: 2px solid var(--RS__underlineBorderColor);
mix-blend-mode: var(--RS__highlightMixBlendMode) !important;
z-index: 1;
}
.R2_CLASS_HIGHLIGHT_AREA[data-marker="1"]:hover,
.R2_CLASS_HIGHLIGHT_AREA[data-marker="1"].hover {
background-color: var(--RS__underlineHoverColor) !important;
}
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="0"] {
background-color: var(--RS__definitionsColor) !important;
mix-blend-mode: var(--RS__definitionsMixBlendMode) !important;
z-index: 1;
}
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="0"]:hover,
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="0"].hover {
background-color: var(--RS__definitionsHoverColor) !important;
}
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="1"] {
border-bottom: 2px solid var(--RS__definitionsUnderlineBorderColor);
mix-blend-mode: var(--RS__definitionsMixBlendMode) !important;
z-index: 1;
}
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="1"]:hover,
#R2_ID_DEFINITIONS_CONTAINER .R2_CLASS_HIGHLIGHT_AREA[data-marker="1"].hover {
background-color: var(--RS__definitionsUnderlineHoverColor) !important;
}
:root[style] .r2-mo-active,
:root .r2-mo-active {
background-color: yellow !important;
color: black !important;
}
:root[style*="readium-night-on"] .r2-mo-active {
background-color: #333333 !important;
color: white !important;
}
:root[style*="readium-sepia-on"] .r2-mo-active {
background-color: silver !important;
color: black !important;
}
.orange {
background: rgba(255, 165, 0, 0.5) !important;
border-bottom: solid 2px rgb(255, 165, 0) !important;
z-index: 2;
position: relative;
}
.red {
background: rgba(255,0,0,0.5) !important;
border-bottom: solid 2px rgb(255,0,0) !important;
z-index: 2;
position: relative;
}
.blue {
background: rgba(0,0,255,0.5) !important;
border-bottom: solid 2px rgb(0,0,255) !important;
z-index: 2;
position: relative;
}
.purple {
background: rgba(102,0,153,0.5) !important;
border-bottom: solid 2px rgb(102,0,153) !important;
z-index: 2;
position: relative;
}
.green {
background: rgba(0,102,0,0.5) !important;
border-bottom: solid 2px rgb(0,102,0) !important;
z-index: 2;
position: relative;
}
.gray {
background: rgba(85,85,85,0.5) !important;
border-bottom: solid 2px rgb(85,85,85) !important;
z-index: 2;
position: relative;
}
.orangetext {
color: rgb(255, 165, 0) !important;
}
.redtext {
color: rgb(255,0,0) !important;
}
.bluetext {
color: rgb(0,0,255) !important;
}
.purpletext {
color: rgb(102,0,153) !important;
}
.greentext {
color: rgb(0,102,0) !important;
}
.graytext {
color: rgb(85,85,85) !important;
}
[data-tts-current-word="true"][data-tts-color="orange"] {
background: rgba(255, 165, 0, 0.5) !important;
border-bottom: solid 2px rgb(255, 165, 0) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="orange"] {
background: rgba(255, 165, 0, 0.5) !important;
z-index: 2;
position: relative;
}
[data-tts-current-word="true"][data-tts-color="red"] {
background: rgba(255,0,0,0.5) !important;
border-bottom: solid 2px rgba(255,0,0) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="red"] {
background: rgba(255,0,0,0.5) !important;
z-index: 2;
position: relative;
}
[data-tts-current-word="true"][data-tts-color="blue"] {
background: rgba(0,0,255,0.5) !important;
border-bottom: solid 2px rgba(0,0,255) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="blue"] {
background: rgba(0,0,255,0.5) !important;
z-index: 2;
position: relative;
}
[data-tts-current-word="true"][data-tts-color="purple"] {
background: rgba(102,0,153,0.5) !important;
border-bottom: solid 2px rgba(102,0,153) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="purple"] {
background: rgba(102,0,153,0.5) !important;
z-index: 2;
position: relative;
}
[data-tts-current-word="true"][data-tts-color="green"] {
background: rgba(0,102,0,0.5) !important;
border-bottom: solid 2px rgba(0,102,0) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="green"] {
background: rgba(0,102,0,0.5) !important;
z-index: 2;
position: relative;
}
[data-tts-current-word="true"][data-tts-color="gray"] {
background: rgba(85,85,85,0.5) !important;
border-bottom: solid 2px rgba(85,85,85) !important;
z-index: 2;
position: relative;
}
[data-tts-current-line="true"][data-tts-color="gray"] {
background: rgba(85,85,85,0.5) !important;
z-index: 2;
position: relative;
}
.icon:hover .icon-tooltip {
display: block;
}
.icon-tooltip {
display: none;
position: absolute;
left: 50%;
/*top: 50%;*/
transform: translate(-50%, -105%);
background: #DADADA;
padding: 0.25rem 0.5rem;
text-overflow: ellipsis;
max-width: 22rem;
min-width: 100px;
z-index: 1000;
border: 1px dotted #5b5852;
}
.icon-tooltip:before, .icon-tooltip:after {
content: '';
display: block;
}
.icon-tooltip:before {
content: " ";
display: block;
position: absolute;
top: 100%;
left: 50%;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 5px solid #5b5852;
margin-left: -10px;
}
img {
max-height: var(--USER__maxMediaHeight) !important;
}
#R2_ID_GUTTER_RIGHT_CONTAINER {
position: absolute;
top: 0;
right: -20px;
}

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Readium
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,905 @@
/* Readium CSS
Config module
A file allowing implementers to customize flags for reading modes,
user settings, etc.
Repo: https://github.com/readium/readium-css */
/* Custom medias
Syntax: @custom-media --variable (prop: value) */
/* Responsive columns
The minimum width for which responsive columns (2 -> 1 and vice versa,
depending on the current font-size) must be enabled */
/* Mobile columns
The minimum and maximum width for mobile devices.
Were forcing the landscape orientation by default,
and must still investigate large tablets (iPad Pro, Surface Pro 3, etc.). */
/* Custom selectors
Syntax: @custom-selector :--variable selector
The selectors you will use for flags/switches
You can alternatively use classes or custom data-* attributes */
/* User view = paged | scrolled */
/* Font-family override */
/* Advanced settings */
/* Reading Modes */
/* Filters (images) */
/* Accessibility normalization */
/* Accessibility font. You can add selectors, using “, ” as a separator, if you have multiple fonts */
/* Direction i.e. ltr and rtl */
/* Readium CSS
Pagination module
A set of styles to paginate ePublications
Repo: https://github.com/readium/readium-css */
/* Config */
/* Columns are responsive by default, even if column-width is set in pixels,
which means two-page spread will switch to single page depending on current font-size.
If you want more control, Im afraid youll have to update colWidth/colGap dynamically,
which is how a significant amount of RS do at the moment. */
/* Default for smartphone portrait (small screens) */
:root {
/* Your columns width floor */
--RS__colWidth: 45em; /* The width at which well switch to 2 columns by default. PS: you cant set it in rem */
/* Ideal number of columns (depending on columns width floor) */
--RS__colCount: 1;
/* Gap between columns (in pixels so that it wont resize with font-size) */
--RS__colGap: 0;
/* Optimal line-length (rem will take :root font-size into account, whatever the bodys font-size) */
--RS__maxLineLength: 40rem;
/* Default page horizontal margins (in pixels so that it wont resize with font-size) */
--RS__pageGutter: 20px; /* See if colGap and pageGutter can be the same var */
}
/* Reset page margins for Forward compatibility */
@page {
margin: 0 !important;
}
/* :root selector has same specificity as a class i.e. 0010
We might have to change that to html / context
-> https://css-tricks.com/almanac/selectors/r/root/ */
:root {
/* In case you use left position to scroll, can be removed if using transform: translateX() */
position: relative;
-webkit-column-width: var(--RS__colWidth);
-moz-column-width: var(--RS__colWidth);
column-width: var(--RS__colWidth);
/* Init pagination */
/* TODO: document columns logic cos it might feel weird at first */
-webkit-column-count: var(--RS__colCount);
-moz-column-count: var(--RS__colCount);
column-count: var(--RS__colCount);
-webkit-column-gap: var(--RS__colGap);
-moz-column-gap: var(--RS__colGap);
column-gap: var(--RS__colGap);
/* Default is balance and we want columns to be filled entirely (100vh) */
-moz-column-fill: auto;
column-fill: auto;
width: 100%;
height: 100vh;
max-width: 100%;
max-height: 100vh;
min-width: 100%;
min-height: 100vh;
padding: 0 !important;
margin: 0 !important;
/* Column size will depend on this if we want to make it responsive */
font-size: 100% !important;
-webkit-text-size-adjust: 100%;
/* Switch to newer box model (not inherited by authors styles) */
box-sizing: border-box;
/* Fix bug for older Chrome */
-webkit-perspective: 1;
/* Prevents options pop-up when long tap in webkit */
-webkit-touch-callout: none;
}
body {
/* overflow: hidden; bugfix: contents wont paginate in Firefox and one sample in Safari */
width: 100%;
/* Limit line-length but we have to reset when 2 columns and control the viewport.
By using max-width + margin auto, margins will shrink when font-size increases,
which is what would be expected in terms of typography. */
max-width: var(--RS__maxLineLength) !important;
padding: 0 var(--RS__pageGutter) !important;
margin: 0 auto !important;
/* We need a minimum padding on body so that descandants/ascendants in italic/script are not cut-off.
Drawback: we have to use border-box so that it doesnt screw the box model,
which means it impacts colWidth and max-width */
box-sizing: border-box;
}
/* Well now redefine margins and columns depending on the minimum width available
The goal is having the simplest model possible and avoid targeting devices */
/* Smartphone landscape */
@media screen and (min-width: 35em) {
:root {
--RS__pageGutter: 30px;
}
}
/* Tablet portrait */
@media screen and (min-width: 45em) {
:root {
--RS__pageGutter: 40px;
}
}
/* Desktop + tablet large */
/* We get the previous settings, we just change the margins */
@media screen and (min-width: 75em) {
:root {
--RS__pageGutter: 50px;
}
}
/* At this point (80em or so), constraining line length must be done at the web view/iframe level, or by limiting the size of :root itself */
/* Responsive columns */
@media screen and (min-width: 60em), screen and (min-device-width: 36em) and (max-device-width: 47em) and (orientation: landscape) {
:root {
/* The size at which we want 2 columns to switch to 1 (depending on font-size) */
--RS__colWidth: 20em; /* 20 * 16 = 320px but 20 * 28 = 560px so it will switch to 1 col @ 175% font-size (user-setting) on an iPad */
/* We constrain to 2 columns so that we can never get 3 or 4, etc. */
--RS__colCount: 2;
--RS__maxLineLength: 39.99rem; /* If we dont use this, colNumber user setting wont work in Safari… */
}
}
/* Readium CSS
Scroll module
A set of styles to scroll ePublications
This module overrides pagination
Repo: https://github.com/readium/readium-css */
:root[style*="readium-scroll-on"] {
/* Reset columns, auto + auto = columns cant be created */
-webkit-columns: auto auto !important;
-moz-columns: auto auto !important;
columns: auto auto !important;
width: auto !important;
height: auto !important;
max-width: none !important;
max-height: none !important;
/* Reset html size so that the user can scroll */
min-width: 0 !important;
min-height: 0 !important;
}
/* Make sure line-length is limited in all configs */
:root[style*="readium-scroll-on"] body {
--RS__maxLineLength: 40rem !important;
margin-top: var(--RS__pageGutter) !important;
margin-bottom: var(--RS__pageGutter) !important;
}
/* Scroll mode horizontal */
/* Vertical writing needs body height set */
/* Do we add a top/bottom margin for body in vertical scroll or not? */
/* Readium CSS
Default highlights
A stylesheet for user highlights
Repo: https://github.com/readium/readium-css */
/* User Highlights */
.readiumCSS-yellow-highlight {
background-color: rgba(255, 255, 0, 0.5);
}
.readiumCSS-green-highlight {
background-color: rgba(0, 255, 0, 0.5);
}
.readiumCSS-orange-highlight {
background-color: rgba(255, 165, 0, 0.5);
}
.readiumCSS-pink-highlight {
background-color: rgba(255, 105, 180, 0.5);
}
/* Media overlays */
.readiumCSS-mo-active-default {
color: black !important;
background-color: yellow !important;
}
/* Readium CSS
Night mode
A preset theme for night mode
Repo: https://github.com/readium/readium-css */
/* CONFIG */
/* [style*="--USER__appearance"] can be used to increase specificity but performance hit */
:root[style*="readium-night-on"] {
--RS__backgroundColor: #000000;
--RS__textColor: #FEFEFE;
--RS__linkColor: #63caff;
--RS__visitedColor: #0099E5;
/* This can be customized but initial will re-use default value of the browser */
--RS__selectionBackgroundColor: #b4d8fe;
--RS__selectionTextColor: inherit;
}
/* we dont need to redeclare bg-color and color for :root since we will inherit and update from day/default mode */
:root[style*="readium-night-on"] *:not(a) {
color: inherit !important;
background-color: transparent !important;
border-color: currentColor !important;
}
:root[style*="readium-night-on"] svg text {
fill: currentColor !important;
stroke: none !important;
}
:root[style*="readium-night-on"] a:link,
:root[style*="readium-night-on"] a:link * {
color: var(--RS__linkColor) !important;
}
:root[style*="readium-night-on"] a:visited,
:root[style*="readium-night-on"] a:visited * {
color: var(--RS__visitedColor) !important;
}
:root[style*="readium-night-on"] img[class*="gaiji"],
:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child {
-webkit-filter: invert(100%);
filter: invert(100%);
}
/* Darken all images on users demand */
:root[style*="readium-night-on"][style*="readium-darken-on"] img {
-webkit-filter: brightness(80%);
filter: brightness(80%);
}
/* Invert all images on users demand */
:root[style*="readium-night-on"][style*="readium-invert-on"] img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
/* Darken and invert on users demand */
:root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img {
-webkit-filter: brightness(80%) invert(100%);
filter: brightness(80%) invert(100%);
}
/* Readium CSS
Sepia mode
A preset theme for sepia mode
Repo: https://github.com/readium/readium-css */
/* CONFIG */
:root[style*="readium-sepia-on"] {
--RS__backgroundColor: #faf4e8;
--RS__textColor: #121212;
--RS__linkColor: #0000EE;
--RS__visitedColor: #551A8B;
/* This can be customized but initial will re-use default value of the browser */
--RS__selectionBackgroundColor: #b4d8fe;
--RS__selectionTextColor: inherit;
--RS__maxLineLength: 40.01rem; /* Forcing a reflow in Blink/Webkit so that blend mode can work */
}
/* we dont need to redeclare bg-color and color for :root since we will inherit and update from day/default mode */
:root[style*="readium-sepia-on"] body {
/* Should be transparent but Chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=711955&q=mix-blend-mode&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified */
color: inherit;
background-color: var(--RS__backgroundColor);
}
:root[style*="readium-sepia-on"] a:link,
:root[style*="readium-sepia-on"] a:link * {
color: var(--RS__linkColor);
}
:root[style*="readium-sepia-on"] a:visited,
:root[style*="readium-sepia-on"] a:visited * {
color: var(--RS__visitedColor);
}
:root[style*="readium-sepia-on"] svg,
:root[style*="readium-sepia-on"] img {
/* Make sure the proper bg-color is used for the blend mode */
background-color: transparent !important;
mix-blend-mode: multiply;
}
/* Readium CSS
OS Accessibility Modes
A stylesheet to deal with OS accessibility settings
Repo: https://github.com/readium/readium-css */
/* Windows high contrast colors are mapped to CSS system color keywords
See http://www.gwhitworth.com/blog/2017/04/how-to-use-ms-high-contrast */
@media screen and (-ms-high-contrast: active) {
:root {
color: windowText !important;
background-color: window !important;
}
/* The following selectors are super funky but it makes sure everything is inherited, this is indeed critical for this mode */
:root :not(#\#):not(#\#):not(#\#),
:root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#)
:root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) {
color: inherit !important;
background-color: inherit !important;
}
.readiumCSS-mo-active-default {
color: highlightText !important;
background-color: highlight !important;
}
/* For links, hyperlink keyword is automatically set */
/* Should we also set user highlights? */
}
@media screen and (-ms-high-contrast: white-on-black) {
:root[style*="readium-night-on"] img[class*="gaiji"],
:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child {
-webkit-filter: none !important;
filter: none !important;
}
:root[style*="readium-night-on"][style*="readium-invert-on"] img {
-webkit-filter: none !important;
filter: none !important;
}
:root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img {
-webkit-filter: brightness(80%);
filter: brightness(80%);
}
}
/* Will be true on recent versions of iOS and MacOS if inverted setting enabled by the user */
@media screen and (inverted-colors) {
:root[style*="readium-night-on"] img[class*="gaiji"],
:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child {
-webkit-filter: none !important;
filter: none !important;
}
:root[style*="readium-night-on"][style*="readium-invert-on"] img {
-webkit-filter: none !important;
filter: none !important;
}
:root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img {
-webkit-filter: brightness(80%);
filter: brightness(80%);
}
}
@media screen and (monochrome) {
/* Grayscale (Implemented in Safari, what about eInk?) */
/* Must deal with anything color (contrast) so must be managed at the night/sepia/theme level :( */
}
@media screen and (prefers-reduced-motion) {
/* If reduced motion is set on MacOS, in case we have animation/transition */
}
/* Readium CSS
Columns number pref
A submodule managing columns number for user settings
Part of “Chrome Advanced” class no flag required.
Repo: https://github.com/readium/readium-css */
/* Number of columns = 1 | 2 */
/* We still need to see if we allow users to force number of columns for all configs, currently it behaves as an "auto" setting */
/* apply col setting except for mobile portrait */
@media screen and (min-width: 60em), screen and (min-device-width: 36em) and (max-device-width: 47em) and (orientation: landscape) {
:root[style*="--USER__colCount: 1"],
:root[style*="--USER__colCount:1"],
:root[style*="--USER__colCount: 2"],
:root[style*="--USER__colCount:2"] {
-webkit-column-count: var(--USER__colCount);
-moz-column-count: var(--USER__colCount);
column-count: var(--USER__colCount);
}
/* If one column, make sure we limit line-length */
:root[style*="--USER__colCount: 1"],
:root[style*="--USER__colCount:1"] {
--RS__maxLineLength: 40rem !important; /* This is the only way for the user setting to work in webkit… */
--RS__colWidth: 100vw;
}
/* If smartphone landscape, and 2 columns, col width the same as iPad landscape + desktop */
:root[style*="--USER__colCount: 2"],
:root[style*="--USER__colCount:2"] {
--RS__colWidth: auto; /* User explicitely tells he/she wants 2 columns, we reset floor value */
}
}
/* Readium CSS
Page margins pref
A submodule managing page margins for user settings
Part of “Chrome Advanced” class no flag required.
Repo: https://github.com/readium/readium-css */
/* Page Margins: the user margin is a factor of the page gutter e.g. 0.5, 0.75, 1, 1.25, 1.5, etc. */
:root[style*="--USER__pageMargins"] body {
padding: 0 calc(var(--RS__pageGutter) * var(--USER__pageMargins)) !important;
}
/* Readium CSS
Custom colors pref
A submodule managing custom colors for user settings
Part of “Chrome Advanced” class no flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="--USER__backgroundColor"] {
background-color: var(--USER__backgroundColor) !important;
}
:root[style*="--USER__backgroundColor"] * {
background-color: transparent !important;
}
:root[style*="--USER__textColor"] {
color: var(--USER__textColor) !important;
}
:root[style*="--USER__textColor"] *:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(pre) {
color: inherit !important;
}
/* Readium CSS
Text align pref
A submodule managing text-align for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__textAlign"] {
text-align: var(--USER__textAlign);
}
:root[style*="readium-advanced-on"][style*="--USER__textAlign"] body,
:root[style*="readium-advanced-on"][style*="--USER__textAlign"] *:not(blockquote):not(figcaption) p,
:root[style*="readium-advanced-on"][style*="--USER__textAlign"] li {
text-align: inherit !important;
-moz-text-align-last: auto !important;
-epub-text-align-last: auto !important;
text-align-last: auto !important;
}
/* In case something goes wrong at the programmatic level + rtl for body + rtl in ltr */
:root[style*="readium-advanced-on"][dir="rtl"][style*="--USER__textAlign: left"],
:root[style*="readium-advanced-on"][dir="rtl"][style*="--USER__textAlign:left"],
:root[style*="readium-advanced-on"][style*="--USER__textAlign: left"] *[dir="rtl"],
:root[style*="readium-advanced-on"][style*="--USER__textAlign:left"] *[dir="rtl"] {
text-align: right;
}
/* Edge, if logical value is used, think of it as a polyfill. For LTR, it will fall back to the default, which is left */
:root[style*="readium-advanced-on"][dir="rtl"][style*="--USER__textAlign: start"],
:root[style*="readium-advanced-on"][dir="rtl"][style*="--USER__textAlign:start"] {
text-align: right;
}
/* Readium CSS
Hyphenation pref
A submodule managing hyphens for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
/* Managing hyphenation automatically for text-align values */
:root[style*="readium-advanced-on"][style*="--USER__textAlign: justify"] body,
:root[style*="readium-advanced-on"][style*="--USER__textAlign:justify"] body {
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
-epub-hyphens: auto;
hyphens: auto;
}
:root[style*="readium-advanced-on"][style*="--USER__textAlign: left"] body,
:root[style*="readium-advanced-on"][style*="--USER__textAlign:left"] body,
:root[style*="readium-advanced-on"][style*="--USER__textAlign: right"] body,
:root[style*="readium-advanced-on"][style*="--USER__textAlign:right"] body {
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
-epub-hyphens: none;
hyphens: none;
}
/* Managing the user override */
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] {
-webkit-hyphens: var(--USER__bodyHyphens) !important;
-moz-hyphens: var(--USER__bodyHyphens) !important;
-ms-hyphens: var(--USER__bodyHyphens) !important;
-epub-hyphens: var(--USER__bodyHyphens) !important;
hyphens: var(--USER__bodyHyphens) !important;
}
/* Sorry, we cant use `:matches`, `:-moz-any` or `:-webkit-any` because MS doesnt support it yet */
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] body,
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] p,
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] li,
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] div,
:root[style*="readium-advanced-on"][style*="--USER__bodyHyphens"] dd {
-webkit-hyphens: inherit;
-moz-hyphens: inherit;
-ms-hyphens: inherit;
-epub-hyphens: inherit;
hyphens: inherit;
}
/* Readium CSS
Font Family pref
A submodule managing font-family for user settings
Part of “User Overrides” class “font override” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-font-on"][style*="--USER__fontFamily"] {
font-family: var(--USER__fontFamily) !important;
}
:root[style*="readium-font-on"][style*="--USER__fontFamily"] body,
:root[style*="readium-font-on"][style*="--USER__fontFamily"] p,
:root[style*="readium-font-on"][style*="--USER__fontFamily"] li,
:root[style*="readium-font-on"][style*="--USER__fontFamily"] div,
:root[style*="readium-font-on"][style*="--USER__fontFamily"] dt,
:root[style*="readium-font-on"][style*="--USER__fontFamily"] dd {
font-family: inherit !important;
}
:root[style*="readium-font-on"][style*="--USER__fontFamily"] i:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] i:not([xml\:lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] em:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] em:not([xml\:lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] cite:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] cite:not([xml\:lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] b:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] b:not([xml\:lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] strong:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] strong:not([xml\:lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] span:not([lang]),
:root[style*="readium-font-on"][style*="--USER__fontFamily"] span:not([xml\:lang]) {
font-family: inherit !important;
}
/* Readium CSS
A11y font pref
A submodule managing a11y text normalization for user settings
Part of “User Overrides” class “font override” flag required.
Repo: https://github.com/readium/readium-css */
/* For AccessibleDfA, we need to normalize font-weight and font-style since only the normal style is available */
:root[style*="readium-font-on"][style*="AccessibleDfA"] {
/* We wont use the variable there since we need fallbacks for missing characters */
font-family: AccessibleDfA, Verdana, Tahoma, "Trebuchet MS", sans-serif !important;
--RS__lineHeightCompensation: 1.167;
}
:root[style*="readium-font-on"][style*="IA Writer Duospace"] {
/* We wont use the variable there since we need fallbacks for missing characters */
font-family: "IA Writer Duospace", Menlo, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", Courier, monospace !important;
--RS__lineHeightCompensation: 1.167;
}
:root[style*="readium-font-on"][style*="readium-a11y-on"] {
font-family: var(--USER__fontFamily) !important;
--RS__lineHeightCompensation: 1.167;
}
/* Maybe users want a setting to normalize any font offered so there is a “a11y Normalize” flag for it */
:root[style*="readium-font-on"][style*="AccessibleDfA"],
:root[style*="readium-font-on"][style*="IA Writer Duospace"],
:root[style*="readium-font-on"][style*="readium-a11y-on"] {
font-style: normal !important;
font-weight: normal !important;
}
/* Targeting everything except code. Note that Open Dyslexic has a monospaced font for code */
:root[style*="readium-font-on"][style*="AccessibleDfA"] *:not(code):not(var):not(kbd):not(samp),
:root[style*="readium-font-on"][style*="IA Writer Duospace"] *:not(code):not(var):not(kbd):not(samp),
:root[style*="readium-font-on"][style*="readium-a11y-on"] *:not(code):not(var):not(kbd):not(samp) {
font-family: inherit !important;
font-style: inherit !important;
font-weight: inherit !important;
}
/* Normalizing text-decoration, subs and sups */
:root[style*="readium-font-on"][style*="AccessibleDfA"] *,
:root[style*="readium-font-on"][style*="IA Writer Duospace"] *,
:root[style*="readium-font-on"][style*="readium-a11y-on"] * {
text-decoration: none !important;
font-variant-caps: normal !important;
font-variant-numeric: normal !important;
font-variant-position: normal !important;
}
:root[style*="readium-font-on"][style*="AccessibleDfA"] sup,
:root[style*="readium-font-on"][style*="IA Writer Duospace"] sup,
:root[style*="readium-font-on"][style*="readium-a11y-on"] sup,
:root[style*="readium-font-on"][style*="AccessibleDfA"] sub,
:root[style*="readium-font-on"][style*="IA Writer Duospace"] sub,
:root[style*="readium-font-on"][style*="readium-a11y-on"] sub {
font-size: 1rem !important;
vertical-align: baseline !important;
}
/* Readium CSS
Font size pref
A submodule managing font-size for user settings
Part of “User Overrides” class no flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="--USER__fontSize"] {
font-size: var(--USER__fontSize) !important;
}
/* Readium CSS
Line height pref
A submodule managing line-height for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__lineHeight"] {
line-height: calc((var(--USER__lineHeight) + (2ex - 1ch) - ((1rem - 16px) * 0.1667)) * var(--RS__lineHeightCompensation)) !important;
}
:root[style*="readium-advanced-on"][style*="--USER__lineHeight"] body,
:root[style*="readium-advanced-on"][style*="--USER__lineHeight"] p,
:root[style*="readium-advanced-on"][style*="--USER__lineHeight"] li,
:root[style*="readium-advanced-on"][style*="--USER__lineHeight"] div {
line-height: inherit;
}
/* Readium CSS
Para spacing pref
A submodule managing paragraphs top and bottom margins for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__paraSpacing"] p {
margin-top: var(--USER__paraSpacing) !important;
margin-bottom: var(--USER__paraSpacing) !important;
}
/* Readium CSS
Para indent pref
A submodule managing paragraphs text-indent for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__paraIndent"] p {
text-indent: var(--USER__paraIndent) !important;
}
/* If there are inline-block elements in paragraphs, text-indent will inherit so we must reset it */
:root[style*="readium-advanced-on"][style*="--USER__paraIndent"] p *,
:root[style*="readium-advanced-on"][style*="--USER__paraIndent"] p:first-letter {
text-indent: 0 !important;
}
/* Readium CSS
Word spacing pref
A submodule managing word-spacing for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h1,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h2,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h3,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h4,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h5,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] h6,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] p,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] li,
:root[style*="readium-advanced-on"][style*="--USER__wordSpacing"] div {
word-spacing: var(--USER__wordSpacing);
}
/* Readium CSS
Letter spacing pref
A submodule managing letter-spacing for user settings
Part of “User Overrides Advanced” class “advanced settings” flag required.
Repo: https://github.com/readium/readium-css */
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h1,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h2,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h3,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h4,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h5,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] h6,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] p,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] li,
:root[style*="readium-advanced-on"][style*="--USER__letterSpacing"] div {
letter-spacing: var(--USER__letterSpacing);
font-variant: none;
}
/* Readium CSS
Font size normalize
A stylesheet to normalize font-size
Repo: https://github.com/readium/readium-css */
/* STYLES */
/* :root is used so that you can quickly add a class or attribute if you prefer e.g. `:root[data-rs-normalize]` */
/* We create a default so that you dont need to explicitly set one in the DOM.
Once the “Publishers styles” checkbox is unchecked, the normalize is applied automatically */
:root[style*="readium-advanced-on"] {
--USER__typeScale: 1.2; /* This is the default type scale youll find in most publications */
}
:root[style*="readium-advanced-on"] p,
:root[style*="readium-advanced-on"] li,
:root[style*="readium-advanced-on"] div,
:root[style*="readium-advanced-on"] pre,
:root[style*="readium-advanced-on"] dd {
font-size: 1rem !important;
}
:root[style*="readium-advanced-on"] h1 {
/* Fallback if browser doesnt support vars */
font-size: 1.75rem !important;
font-size: calc(((1rem * var(--USER__typeScale)) * var(--USER__typeScale)) * var(--USER__typeScale)) !important;
}
:root[style*="readium-advanced-on"] h2 {
/* Fallback if browser doesnt support vars */
font-size: 1.5rem !important;
font-size: calc((1rem * var(--USER__typeScale)) * var(--USER__typeScale)) !important;
}
:root[style*="readium-advanced-on"] h3 {
/* Fallback if browser doesnt support vars */
font-size: 1.25rem !important;
font-size: calc(1rem * var(--USER__typeScale)) !important;
}
:root[style*="readium-advanced-on"] h4,
:root[style*="readium-advanced-on"] h5,
:root[style*="readium-advanced-on"] h6 {
font-size: 1rem !important;
}
:root[style*="readium-advanced-on"] small {
font-size: smaller !important;
}
:root[style*="readium-advanced-on"] sub,
:root[style*="readium-advanced-on"] sup {
font-size: 67.5% !important;
}
/* The following styles kick in if you define the typeScale variable in the DOM.
No need to repeat declarations which dont make use of the variable */
:root[style*="readium-advanced-on"][style*="--USER__typeScale"] h1 {
font-size: calc(((1rem * var(--USER__typeScale)) * var(--USER__typeScale)) * var(--USER__typeScale)) !important;
}
:root[style*="readium-advanced-on"][style*="--USER__typeScale"] h2 {
font-size: calc((1rem * var(--USER__typeScale)) * var(--USER__typeScale)) !important;
}
:root[style*="readium-advanced-on"][style*="--USER__typeScale"] h3 {
font-size: calc(1rem * var(--USER__typeScale)) !important;
}

View file

@ -0,0 +1,602 @@
/* Readium CSS
Config module
A file allowing implementers to customize flags for reading modes,
user settings, etc.
Repo: https://github.com/readium/readium-css */
/* Custom medias
Syntax: @custom-media --variable (prop: value) */
/* Responsive columns
The minimum width for which responsive columns (2 -> 1 and vice versa,
depending on the current font-size) must be enabled */
/* Mobile columns
The minimum and maximum width for mobile devices.
Were forcing the landscape orientation by default,
and must still investigate large tablets (iPad Pro, Surface Pro 3, etc.). */
/* Custom selectors
Syntax: @custom-selector :--variable selector
The selectors you will use for flags/switches
You can alternatively use classes or custom data-* attributes */
/* User view = paged | scrolled */
/* Font-family override */
/* Advanced settings */
/* Reading Modes */
/* Filters (images) */
/* Accessibility normalization */
/* Accessibility font. You can add selectors, using “, ” as a separator, if you have multiple fonts */
/* Direction i.e. ltr and rtl */
/* Readium CSS
Base module
A minimal stylesheet for all ebooks
Repo: https://github.com/readium/readium-css */
@namespace url("http://www.w3.org/1999/xhtml");
@namespace epub url("http://www.idpf.org/2007/ops");
@namespace m url("http://www.w3.org/1998/Math/MathML/");
@namespace svg url("http://www.w3.org/2000/svg");
/* Define viewport, HTML5-style */
@-ms-viewport {
width: device-width;
}
@viewport {
width: device-width;
zoom: 1;
}
:root {
/* Default font-stacks */
--RS__oldStyleTf: "Iowan Old Style", "Sitka Text", Palatino, "Book Antiqua", serif;
--RS__modernTf: Athelas, Constantia, Georgia, serif;
--RS__sansTf: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--RS__humanistTf: Seravek, Calibri, Roboto, Arial, sans-serif;
--RS__monospaceTf: "Andale Mono", Consolas, monospace;
/* Config */
--RS__baseFontFamily: var(--RS__oldStyleTf);
/* For square-ish fonts (CJK, Indic, etc.), we must apply some compensation in dynamic leading. Default is 1 i.e. no compensation */
--RS__lineHeightCompensation: 1;
/* Dynamic leading based on typeface metrics + font-size setting */
--RS__baseLineHeight: calc((1em + (2ex - 1ch) - ((1rem - 16px) * 0.1667)) * var(--RS__lineHeightCompensation));
}
/* Set default font for the html doc, so that it can be overridden by the authorss stylesheet */
html {
font-family: var(--RS__baseFontFamily);
/* Fallback line-height */
line-height: 1.6; /* Fits a little bit better for all languages than 1.5 */
line-height: var(--RS__baseLineHeight);
text-rendering: optimizeLegibility;
}
/* 1.5 being too loose with larger font-sizes, we reset headings to normal (which value is 1.1251.375 for our font-stacks) */
h1, h2, h3 {
line-height: normal;
}
:lang(ja),
:lang(zh),
:lang(ko) {
word-wrap: break-word;
-webkit-line-break: strict;
-epub-line-break: strict;
line-break: strict;
}
/* Set default font for Math */
math {
font-family: "Latin Modern Math", "STIX Two Math", "XITS Math", "STIX Math", "Libertinus Math", "TeX Gyre Termes Math", "TeX Gyre Bonum Math", "TeX Gyre Schola", "DejaVu Math TeX Gyre", "TeX Gyre Pagella Math", "Asana Math", "Cambria Math", "Lucida Bright Math", "Minion Math", STIXGeneral, STIXSizeOneSym, Symbol, "Times New Roman", serif;
}
/* Language Overrides
That will only work if either html or body have a (xml:)lang attribute, not for inline overrides */
:lang(am) {
--RS__baseFontFamily: Kefa, Nyala, Roboto, Noto, "Noto Sans Ethiopic", serif;
--RS__lineHeightCompensation: 1.167;
}
:lang(ar) {
--RS__baseFontFamily: "Geeza Pro", "Arabic Typesetting", Roboto, Noto, "Noto Naskh Arabic", "Times New Roman", serif;
}
:lang(bn) {
--RS__baseFontFamily: "Kohinoor Bangla", "Bangla Sangam MN", Vrinda, Roboto, Noto, "Noto Sans Bengali", sans-serif;
--RS__lineHeightCompensation: 1.067;
}
:lang(bo) {
--RS__baseFontFamily: Kailasa, "Microsoft Himalaya", Roboto, Noto, "Noto Sans Tibetan", sans-serif;
}
:lang(chr) {
--RS__baseFontFamily: "Plantagenet Cherokee", Roboto, Noto, "Noto Sans Cherokee";
--RS__lineHeightCompensation: 1.167;
}
:lang(fa) {
--RS__baseFontFamily: "Geeza Pro", "Arabic Typesetting", Roboto, Noto, "Noto Naskh Arabic", "Times New Roman", serif;
}
:lang(gu) {
--RS__baseFontFamily: "Gujarati Sangam MN", "Nirmala UI", Shruti, Roboto, Noto, "Noto Sans Gujarati", sans-serif;
--RS__lineHeightCompensation: 1.167;
}
:lang(he) {
--RS__baseFontFamily: "New Peninim MT", "Arial Hebrew", Gisha, "Times New Roman", Roboto, Noto, "Noto Sans Hebrew" sans-serif;
--RS__lineHeightCompensation: 1.1;
}
:lang(hi) {
--RS__baseFontFamily: "Kohinoor Devanagari", "Devanagari Sangam MN", Kokila, "Nirmala UI", Roboto, Noto, "Noto Sans Devanagari", sans-serif;
--RS__lineHeightCompensation: 1.1;
}
:lang(hy) {
--RS__baseFontFamily: Mshtakan, Sylfaen, Roboto, Noto, "Noto Serif Armenian", serif;
}
:lang(iu) {
--RS__baseFontFamily: "Euphemia UCAS", Euphemia, Roboto, Noto, "Noto Sans Canadian Aboriginal", sans-serif;
}
:lang(ja) {
--RS__baseFontFamily: "游ゴシック体", YuGothic, "ヒラギノ丸ゴ", "Hiragino Sans", "Yu Gothic UI", "Meiryo UI", "MS Gothic", Roboto, Noto, "Noto Sans CJK JP", sans-serif;
/* For CJK, the line-height is usually 1520% more than for Latin */
--RS__lineHeightCompensation: 1.167;
/* Extra variables for Japanese font-stacks as we may want to reuse them for user settings + default */
--RS__serif-ja: " P明朝", "MS PMincho", "Hiragino Mincho Pro", "ヒラギノ明朝 Pro W3", "游明朝", "YuMincho", " 明朝", "MS Mincho", "Hiragino Mincho ProN", serif;
--RS__sans-serif-ja: " Pゴシック", "MS PGothic", "Hiragino Kaku Gothic Pro W3", "ヒラギノ角ゴ Pro W3", "Hiragino Sans GB", "ヒラギノ角ゴシック W3", "游ゴシック", "YuGothic", " ゴシック", "MS Gothic", "Hiragino Sans", sans-serif;
--RS__serif-ja-v: " 明朝", "MS Mincho", "Hiragino Mincho Pro", "ヒラギノ明朝 Pro W3", "游明朝", "YuMincho", " P明朝", "MS PMincho", "Hiragino Mincho ProN", serif;
--RS__sans-serif-ja-v: " ゴシック", "MS Gothic", "Hiragino Kaku Gothic Pro W3", "ヒラギノ角ゴ Pro W3", "Hiragino Sans GB", "ヒラギノ角ゴシック W3", "游ゴシック", "YuGothic", " Pゴシック", "MS PGothic", "Hiragino Sans", sans-serif;
}
:lang(km) {
--RS__baseFontFamily: "Khmer Sangam MN", "Leelawadee UI", "Khmer UI", Roboto, Noto, "Noto Sans Khmer", sans-serif;
--RS__lineHeightCompensation: 1.067;
}
:lang(kn) {
--RS__baseFontFamily: "Kannada Sangam MN", "Nirmala UI", Tunga, Roboto, Noto, "Noto Sans Kannada", sans-serif;
--RS__lineHeightCompensation: 1.1;
}
:lang(ko) {
--RS__baseFontFamily: "Nanum Gothic", "Apple SD Gothic Neo", "Malgun Gothic", Roboto, Noto, "Noto Sans CJK KR", sans-serif;
/* For CJK, the line-height is usually 1520% more than for Latin */
--RS__lineHeightCompensation: 1.167;
}
:lang(lo) {
--RS__baseFontFamily: "Lao Sangam MN", "Leelawadee UI", "Lao UI", Roboto, Noto, "Noto Sans Lao", sans-serif;
}
:lang(ml) {
--RS__baseFontFamily: "Malayalam Sangam MN", "Nirmala UI", Kartika, Roboto, Noto, "Noto Sans Malayalam", sans-serif;
--RS__lineHeightCompensation: 1.067;
}
:lang(or) {
--RS__baseFontFamily: "Oriya Sangam MN", "Nirmala UI", Kalinga, Roboto, Noto, "Noto Sans Oriya", sans-serif;
--RS__lineHeightCompensation: 1.167;
}
:lang(pa) {
--RS__baseFontFamily: "Gurmukhi MN", "Nirmala UI", Kartika, Roboto, Noto, "Noto Sans Gurmukhi", sans-serif;
--RS__lineHeightCompensation: 1.1;
}
:lang(si) {
--RS__baseFontFamily: "Sinhala Sangam MN", "Nirmala UI", "Iskoola Pota", Roboto, Noto, "Noto Sans Sinhala", sans-serif;
--RS__lineHeightCompensation: 1.167;
}
:lang(ta) {
--RS__baseFontFamily: "Tamil Sangam MN", "Nirmala UI", Latha, Roboto, Noto, "Noto Sans Tamil", sans-serif;
--RS__lineHeightCompensation: 1.067;
}
:lang(te) {
--RS__baseFontFamily: "Kohinoor Telugu", "Telugu Sangam MN", "Nirmala UI", Gautami, Roboto, Noto, "Noto Sans Telugu", sans-serif;
}
:lang(th) {
--RS__baseFontFamily: "Thonburi", "Leelawadee UI", "Cordia New", Roboto, Noto, "Noto Sans Thai", sans-serif;
--RS__lineHeightCompensation: 1.067;
}
/* The following will also work for zh-Hans */
:lang(zh) {
--RS__baseFontFamily: "方体", "PingFang SC", "黑体", "Heiti SC", "Microsoft JhengHei UI", "Microsoft JhengHei", Roboto, Noto, "Noto Sans CJK SC", sans-serif;
/* For CJK, the line-height is usually 1520% more than for Latin */
--RS__lineHeightCompensation: 1.167;
}
:lang(zh-Hant),
:lang(zh-TW) {
--RS__baseFontFamily: "方體", "PingFang TC", "黑體", "Heiti TC", "Microsoft JhengHei UI", "Microsoft JhengHei", Roboto, Noto, "Noto Sans CJK TC", sans-serif;
/* For CJK, the line-height is usually 1520% more than for Latin */
--RS__lineHeightCompensation: 1.167;
}
:lang(zh-HK) {
--RS__baseFontFamily: "方體", "PingFang HK", "方體", "PingFang TC", "黑體", "Heiti TC", "Microsoft JhengHei UI", "Microsoft JhengHei", Roboto, Noto, "Noto Sans CJK TC", sans-serif;
/* For CJK, the line-height is usually 1520% more than for Latin */
--RS__lineHeightCompensation: 1.167;
}
/* Readium CSS
Day/Default mode
A preset theme for day mode, which is the default
Repo: https://github.com/readium/readium-css */
/* CONFIG */
:root {
--RS__backgroundColor: #FFFFFF;
--RS__textColor: #121212;
/* This can be customized but initial will re-use default value of the browser */
--RS__selectionBackgroundColor: #b4d8fe;
--RS__selectionTextColor: inherit;
}
:root {
color: var(--RS__textColor) !important;
background-color: var(--RS__backgroundColor) !important;
}
/* Note: Though `::selection` was present in drafts of CSS Selectors Level 3, it was removed during the Candidate Recommendation phase because its behavior was under-specified (especially with nested elements) and interoperability wasnt achieved. Source: https://developer.mozilla.org/en-US/docs/Web/CSS/::selection */
::-moz-selection {
color: var(--RS__selectionTextColor);
background-color: var(--RS__selectionBackgroundColor);
}
::selection {
color: var(--RS__selectionTextColor);
background-color: var(--RS__selectionBackgroundColor);
}
/* Readium CSS
Fonts module
A stylesheet for embedded fonts
Repo: https://github.com/readium/readium-css */
/* /!\ Mind the path (relative to the folders in which you have stylesheets and the fonts) */
/*@font-face {*/
/* font-family: AccessibleDfA;*/
/* font-style: normal;*/
/* font-weight: normal;*/
/* src: local("AccessibleDfA"),*/
/* url("fonts/AccessibleDfA.otf") format("opentype");*/
/*}*/
/*@font-face {*/
/* font-family: "IA Writer Duospace";*/
/* font-style: normal;*/
/* font-weight: normal;*/
/* src: local("iAWriterDuospace-Regular"),*/
/* url("fonts/iAWriterDuospace-Regular.ttf") format("truetype");*/
/*}*/
/* If you have different weights/styles,
use `font-weight` and `font-style`,
not prefixes in the font-family name,
or else it will be a nightmare to manage in user settings. */
/* Readium CSS
HTML5 SR Patch stylesheet
A set of style to adjust HTML5 Suggested Rendering to paginated content
Repo: https://github.com/readium/readium-css */
/* Fragmentation */
body {
widows: 2;
orphans: 2;
}
figcaption, th, td {
widows: 1;
orphans: 1;
}
h2, h3, h4, h5, h6, dt,
hr, caption {
-webkit-column-break-after: avoid;
page-break-after: avoid;
break-after: avoid;
}
h1, h2, h3, h4, h5, h6, dt,
figure, tr {
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
/* Hyphenation */
body {
-webkit-hyphenate-character: "\002D";
-moz-hyphenate-character: "\002D";
-ms-hyphenate-character: "\002D";
hyphenate-character: "\002D";
-webkit-hyphenate-limit-lines: 3;
-ms-hyphenate-limit-lines: 3;
hyphenate-limit-lines: 3;
}
h1, h2, h3, h4, h5, h6, dt,
figcaption, pre, caption, address,
center, code, var {
-ms-hyphens: none;
-moz-hyphens: none;
-webkit-hyphens: none;
-epub-hyphens: none;
hyphens: none;
}
/* OTF */
body {
font-variant-numeric: oldstyle-nums proportional-nums;
}
:lang(ja) body,
:lang(zh) body,
:lang(ko) body {
font-variant-numeric: lining-nums proportional-nums;
}
h1, h2, h3, h4, h5, h6, dt {
font-variant-numeric: lining-nums proportional-nums;
}
table {
font-variant-numeric: lining-nums tabular-nums;
}
code, var {
font-variant-ligatures: none;
font-variant-numeric: lining-nums tabular-nums slashed-zero;
}
rt {
font-variant-east-asian: ruby;
}
:lang(ar) {
font-variant-ligatures: common-ligatures;
}
:lang(ko) {
font-kerning: normal;
}
/* Night mode */
hr {
color: inherit;
border-color: currentColor;
}
table, th, td {
border-color: currentColor;
}
/* Horizontal Spacing */
figure, blockquote {
margin: 1em 5%;
}
/*
:lang(ja) figure, :lang(ja) blockquote,
:lang(zh-Hant) figure, :lang(zh-Hant) blockquote,
:lang(zh-TW) figure, :lang(zh-TW) blockquote,
:lang(mn) figure, :lang(mn) blockquote {
margin: 5% 1em;
}
*/
ul, ol {
padding-left: 5%;
}
/*
:lang(ja) ul, :lang(ja) ol,
:lang(zh-Hant) ul, :lang(zh-Hant) ol,
:lang(zh-TW) ul, :lang(zh-TW) ol,
:lang(mn) ul, :lang(mn) ol {
padding-top: 5%;
}
*/
dd {
margin-left: 5%;
}
/*
:lang(ja) dd,
:lang(zh-Hant) dd,
:lang(zh-TW) dd,
:lang(mn) dd {
margin-top: 5%;
}
*/
pre {
white-space: pre-wrap;
-ms-tab-size: 2;
-moz-tab-size: 2;
-webkit-tab-size: 2;
tab-size: 2;
}
/* Normalization */
abbr[title], acronym[title] {
text-decoration: dotted underline;
}
nobr wbr {
white-space: normal;
}
/* Make ruby text and parentheses non-selectable (TBC) */
ruby > rt, ruby > rp {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Internationalization */
*:lang(ja),
*:lang(zh),
*:lang(ko),
:lang(ja) cite, :lang(ja) dfn, :lang(ja) em, :lang(ja) i,
:lang(zh) cite, :lang(zh) dfn, :lang(zh) em, :lang(zh) i,
:lang(ko) cite, :lang(ko) dfn, :lang(ko) em, :lang(ko) i {
font-style: normal;
}
:lang(ja) a,
:lang(zh) a,
:lang(ko) a {
text-decoration: none;
}
/* Readium CSS
Safeguards module
A set of styles to prevent common issues in pagination
Repo: https://github.com/readium/readium-css */
/* Config */
/* Well be using an "RS__" prefix so that we can prevent collisions with authors CSS */
:root {
/* max-width for media, you can override that via JS if not compiled to static */
--RS__maxMediaWidth: 100%;
/* max-height for media, you can override that via JS if not compiled to static
Please consider figures might have a figcaption, which is why 95vh in the first place */
--RS__maxMediaHeight: 95vh;
/* value for medias box-sizing */
--RS__boxSizingMedia: border-box;
/* value for tables box-sizing */
--RS__boxSizingTable: border-box;
}
/* Sanitize line-heights in webkit e.g. raised cap without a declared line-height
See effect by checking this demo in Safari: https://codepen.io/JayPanoz/pen/gRmzrE
Note: glyphs has to be reset to inline for CJK */
html {
-webkit-line-box-contain: block glyphs replaced;
}
:lang(ja) {
-webkit-line-box-contain: block inline replaced;
}
/* Wrap long strings if larger than line-length */
a, h1, h2, h3, h4, h5, h6 {
word-wrap: break-word;
}
div {
max-width: var(--RS__maxMediaWidth);
}
/* Size medias */
/* You can override CSS variables by re-defining it for all elements or a specific one */
img, svg, audio, video {
/* Object-fit allows us to keep the correct aspect-ratio */
object-fit: contain;
width: auto;
height: auto;
/* Some files dont have max-width */
max-width: var(--RS__maxMediaWidth);
/* Were setting a max-height, especially for covers */
max-height: var(--RS__maxMediaHeight) !important;
/* We probably dont need to use modern box-sizing as auto behaves like it */
box-sizing: var(--RS__boxSizingMedia);
/* For page-break, we must use those 3
We cant use a variable there, webkit seems to no support them for those properties */
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
/* Try preventing border being cut-off, webkit + blink have content-box by default */
table {
max-width: var(--RS__maxMediaWidth);
box-sizing: var(--RS__boxSizingTable);
}

View file

@ -0,0 +1,169 @@
/* Readium CSS
Default module
A stylesheet for unstyled ebooks based on HTML5 Suggested Rendering
Note: works in combination with Base module
Repo: https://github.com/readium/readium-css */
/* CONFIG */
:root {
--RS__compFontFamily: var(--RS__baseFontFamily);
--RS__codeFontFamily: var(--RS__monospaceTf);
--RS__typeScale: 1.125; /* 1.067 | 1.125 | 1.2 | 1.25 | 1.333 | 1.414 | 1.5 | 1.618 */
--RS__baseFontSize: 100%;
--RS__flowSpacing: 1.5rem;
--RS__paraSpacing: 0;
--RS__paraIndent: 1em;
--RS__linkColor: #0000EE;
--RS__visitedColor: #551A8B;
--RS__primaryColor: ;
--RS__secondaryColor: ;
}
/* STYLES */
/* Typo */
mark {
background: yellow;
}
mark.current {
background: orange;
}
body {
font-size: var(--RS__baseFontSize);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--RS__compFontFamily);
}
/* Flow content */
blockquote, figure, p, pre,
aside, footer, form, hr {
margin-top: var(--RS__flowSpacing);
margin-bottom: var(--RS__flowSpacing);
}
p {
margin-top: var(--RS__paraSpacing);
margin-bottom: var(--RS__paraSpacing);
text-indent: var(--RS__paraIndent);
}
h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p,
hr + p {
text-indent: 0;
}
pre {
font-family: var(--RS__codeFontFamily);
}
/* Phrasing content */
code, kbd, samp, tt {
font-family: var(--RS__codeFontFamily);
}
sub, sup {
position: relative;
font-size: 67.5%;
line-height: 1;
}
sub {
bottom: -0.2ex;
}
sup {
bottom: 0;
}
:link {
color: var(--RS__linkColor);
}
:visited {
color: var(--RS__visitedColor);
}
/* Headings */
h1 {
margin-top: calc(var(--RS__flowSpacing) * 2);
margin-bottom: calc(var(--RS__flowSpacing) * 2);
/* The following is base font size * typescale power of 3 */
font-size: calc(((1em * var(--RS__typeScale)) * var(--RS__typeScale)) * var(--RS__typeScale));
}
h2 {
margin-top: calc(var(--RS__flowSpacing) * 2);
margin-bottom: var(--RS__flowSpacing);
/* The following is base font size * typescale power of 2 */
font-size: calc((1em * var(--RS__typeScale)) * var(--RS__typeScale));
}
h3 {
margin-top: var(--RS__flowSpacing);
margin-bottom: var(--RS__flowSpacing);
font-size: calc(1em * var(--RS__typeScale));
}
h4 {
margin-top: var(--RS__flowSpacing);
margin-bottom: var(--RS__flowSpacing);
font-size: 1em;
}
h5 {
margin-top: var(--RS__flowSpacing);
margin-bottom: var(--RS__flowSpacing);
font-size: 1em;
font-variant: small-caps;
}
h6 {
margin-top: var(--RS__flowSpacing);
margin-bottom: 0;
font-size: 1em;
text-transform: lowercase;
font-variant: small-caps;
}
/* Lists */
dl, ol, ul {
margin-top: var(--RS__flowSpacing);
margin-bottom: var(--RS__flowSpacing);
}
/* Table */
table {
margin: var(--RS__flowSpacing) 0;
border: 1px solid currentColor;
border-collapse: collapse;
empty-cells: show;
}
thead, tbody, tfoot, table > tr {
vertical-align: top;
}
th {
text-align: left;
}
th, td {
padding: 4px;
border: 1px solid currentColor;
}

View file

@ -0,0 +1,7 @@
export interface TocEntry {
title: string,
href?: string,
children?: TocEntry[],
current?: boolean,
level?: number,
}

View file

@ -6,6 +6,7 @@ import i18n from '@/i18n'
import {MediaStatus} from '@/types/enum-books'
import {getFileSize} from '@/functions/file'
import {ReadListDto} from '@/types/komga-readlists'
import {getBookReadRouteFromMediaProfile} from '@/functions/book-format'
export enum ItemTypes {
BOOK, SERIES, COLLECTION, READLIST
@ -169,7 +170,7 @@ export class BookItem extends Item<BookDto> {
fabTo(): RawLocation {
return {
name: 'read-book',
name: getBookReadRouteFromMediaProfile(this.item?.media?.mediaProfile),
params: {bookId: this.item.id},
query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
}

View file

@ -27,7 +27,8 @@ export interface MediaDto {
status: string,
mediaType: string,
pagesCount: number,
comment: string
comment: string,
mediaProfile: string,
}
export interface PageDto {

View file

@ -339,6 +339,7 @@ import {SeriesDto} from '@/types/komga-series'
import jsFileDownloader from 'js-file-downloader'
import screenfull from 'screenfull'
import {ItemTypes} from '@/types/items'
import {getBookReadRouteFromMediaProfile} from '@/functions/book-format'
export default Vue.extend({
name: 'BookReader',
@ -741,7 +742,7 @@ export default Vue.extend({
if (!this.$_.isEmpty(this.siblingPrevious)) {
this.jumpToPreviousBook = false
this.$router.push({
name: 'read-book',
name: getBookReadRouteFromMediaProfile(this.siblingPrevious.media.mediaProfile),
params: {bookId: this.siblingPrevious.id.toString()},
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
})
@ -753,7 +754,7 @@ export default Vue.extend({
} else {
this.jumpToNextBook = false
this.$router.push({
name: 'read-book',
name: getBookReadRouteFromMediaProfile(this.siblingNext.media.mediaProfile),
params: {bookId: this.siblingNext.id.toString()},
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
})

View file

@ -194,7 +194,7 @@
<v-btn color="accent"
small
:title="$t('browse_book.read_book')"
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id}}"
:to="{name: readRouteName, params: { bookId: bookId}, query: { context: context.origin, contextId: context.id}}"
:disabled="!canRead"
>
<v-icon left small>mdi-book-open-page-variant</v-icon>
@ -205,7 +205,7 @@
<v-col cols="auto">
<v-btn small
:title="$t('browse_book.read_incognito')"
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:to="{name: readRouteName, params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:disabled="!canRead"
>
<v-icon left small>mdi-incognito</v-icon>
@ -240,7 +240,7 @@
<v-btn color="accent"
small
:title="$t('browse_book.read_book')"
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id}}"
:to="{name: readRouteName, params: { bookId: bookId}, query: { context: context.origin, contextId: context.id}}"
:disabled="!canRead"
>
<v-icon left small>mdi-book-open-page-variant</v-icon>
@ -251,7 +251,7 @@
<v-col cols="auto">
<v-btn small
:title="$t('browse_book.read_incognito')"
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:to="{name: readRouteName, params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:disabled="!canRead"
>
<v-icon left small>mdi-incognito</v-icon>
@ -409,7 +409,7 @@ import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
import ItemCard from '@/components/ItemCard.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import {groupAuthorsByRole} from '@/functions/authors'
import {getBookFormatFromMediaType} from '@/functions/book-format'
import {getBookFormatFromMediaType, getBookReadRouteFromMediaProfile} from '@/functions/book-format'
import {getPagesLeft, getReadProgress, getReadProgressPercentage} from '@/functions/book-progress'
import {getBookTitleCompact} from '@/functions/book-title'
import {bookFileUrl, bookThumbnailUrl} from '@/functions/urls'
@ -487,6 +487,9 @@ export default Vue.extend({
next()
},
computed: {
readRouteName(): string {
return getBookReadRouteFromMediaProfile(this.book.media.mediaProfile)
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},

View file

@ -217,7 +217,7 @@
<v-btn color="accent"
small
:title="$t('browse_book.read_book')"
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
:to="{name: readRouteName, params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
:disabled="!canRead"
>
<v-icon left small>mdi-book-open-page-variant</v-icon>
@ -228,7 +228,7 @@
<v-col cols="auto">
<v-btn small
:title="$t('browse_book.read_incognito')"
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:to="{name: readRouteName, params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:disabled="!canRead"
>
<v-icon left small>mdi-incognito</v-icon>
@ -263,7 +263,7 @@
<v-btn color="accent"
small
:title="$t('browse_book.read_book')"
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
:to="{name: readRouteName, params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
:disabled="!canRead"
>
<v-icon left small>mdi-book-open-page-variant</v-icon>
@ -274,7 +274,7 @@
<v-col cols="auto">
<v-btn small
:title="$t('browse_book.read_incognito')"
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:to="{name: readRouteName, params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
:disabled="!canRead"
>
<v-icon left small>mdi-incognito</v-icon>
@ -484,7 +484,7 @@ import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
import ItemCard from '@/components/ItemCard.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import {groupAuthorsByRole} from '@/functions/authors'
import {getBookFormatFromMediaType} from '@/functions/book-format'
import {getBookFormatFromMediaType, getBookReadRouteFromMediaProfile} from '@/functions/book-format'
import {getPagesLeft, getReadProgress, getReadProgressPercentage} from '@/functions/book-progress'
import {getBookTitleCompact} from '@/functions/book-title'
import {bookFileUrl, seriesThumbnailUrl} from '@/functions/urls'
@ -596,6 +596,9 @@ export default Vue.extend({
next()
},
computed: {
readRouteName(): string {
return getBookReadRouteFromMediaProfile(this.book.media.mediaProfile)
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},

View file

@ -0,0 +1,821 @@
<template>
<div id="root" :key="bookId">
<v-slide-y-transition>
<v-toolbar
v-if="showToolbars"
dense elevation="1"
class="settings full-width"
style="position: fixed; top: 0;z-index: 14"
>
<v-btn
icon
@click="closeBook"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-btn
:disabled="!hasToc && !hasLandmarks && !hasPageList"
icon
@click="showToc = !showToc">
<v-icon>mdi-table-of-contents</v-icon>
</v-btn>
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn
icon
:disabled="!screenfull.isEnabled"
@click="screenfull.isFullscreen ? screenfull.exit() : enterFullscreen()">
<v-icon>{{ fullscreenIcon }}</v-icon>
</v-btn>
<v-btn
icon
@click="showHelp = !showHelp">
<v-icon>mdi-help-circle</v-icon>
</v-btn>
<!-- <v-btn-->
<!-- icon-->
<!-- @click="showExplorer = !showExplorer"-->
<!-- >-->
<!-- <v-icon>mdi-view-grid</v-icon>-->
<!-- </v-btn>-->
<v-btn
icon
@click="toggleSettings"
>
<v-icon>mdi-cog</v-icon>
</v-btn>
</v-toolbar>
</v-slide-y-transition>
<v-slide-y-reverse-transition>
<!-- Bottom Toolbar-->
<v-toolbar
dense
elevation="1"
class="settings full-width"
style="position: fixed; bottom: 0;z-index: 14"
horizontal
v-if="showToolbars"
>
<v-btn icon @click="previousBook">
<v-icon>mdi-undo</v-icon>
</v-btn>
<v-spacer/>
<v-btn
icon
:disabled="!historyCanGoBack"
@click="historyBack"
>
<v-icon>mdi-chevron-left</v-icon>
</v-btn>
<v-btn
icon
:disabled="!historyCanGoForward"
@click="historyForward"
>
<v-icon>mdi-chevron-right</v-icon>
</v-btn>
<v-spacer/>
<v-btn icon @click="nextBook">
<v-icon>mdi-redo</v-icon>
</v-btn>
</v-toolbar>
</v-slide-y-reverse-transition>
<v-navigation-drawer
v-model="showToc"
fixed
temporary
:width="$vuetify.breakpoint.smAndUp ? 500 : $vuetify.breakpoint.width - 50"
style="z-index: 15"
>
<v-tabs grow>
<v-tab v-if="hasToc">
<v-icon>mdi-table-of-contents</v-icon>
</v-tab>
<v-tab v-if="hasLandmarks">
<v-icon>mdi-eiffel-tower</v-icon>
</v-tab>
<v-tab v-if="hasPageList">
<v-icon>mdi-numeric</v-icon>
</v-tab>
<v-tab-item v-if="hasToc" class="scrolltab">
<toc-list :toc="tableOfContents" @goto="goToEntry" class="scrolltab-content"/>
</v-tab-item>
<v-tab-item v-if="hasLandmarks" class="scrolltab">
<toc-list :toc="landmarks" @goto="goToEntry" class="scrolltab-content"/>
</v-tab-item>
<v-tab-item v-if="hasPageList" class="scrolltab">
<toc-list :toc="pageList" @goto="goToEntry" class="scrolltab-content"/>
</v-tab-item>
</v-tabs>
</v-navigation-drawer>
<header id="headerMenu"/>
<div id="D2Reader-Container" style="height: 100vh" :class="appearanceClass('bg')">
<main tabindex=-1 id="iframe-wrapper" style="height: 100vh">
<div id="reader-loading"></div>
<div id="reader-error"></div>
<div id="reader-info-top">
<span class="book-title"></span>
</div>
<div id="reader-info-bottom">
<div style="display: flex;justify-content: center;">
<span id="chapter-position"></span>&nbsp;
<span id="chapter-title"></span>
</div>
</div>
</main>
<a id="previous-chapter" rel="prev" role="button" aria-labelledby="previous-label"
style="left: 50%;position: fixed;color: #000;height: 24px;background: #d3d3d33b; width: 150px;transform: translate(-50%, 0); display: block"
:style="`top: ${showToolbars ? 48 : 0}px`"
>
<v-icon style="left: calc(50% - 12px); position: relative;">mdi-chevron-up</v-icon>
</a>
<a id="next-chapter" rel="next" role="button" aria-labelledby="next-label"
style="bottom: 0;left: 50%;position: fixed;color: #000;height: 24px;background: #d3d3d33b; width: 150px;transform: translate(-50%, 0); display: block">
<v-icon style="left: calc(50% - 12px);position: relative;">mdi-chevron-down</v-icon>
</a>
</div>
<footer id="footerMenu">
<a rel="prev" class="disabled" role="button" aria-labelledby="previous-label" style="top: 50%;left:0;position: fixed;height: 100px;
background: #d3d3d33b;">
<v-icon style="top: calc(50% - 12px);
position: relative;">mdi-chevron-left
</v-icon>
</a>
<a rel="next" class="disabled" role="button" aria-labelledby="next-label" style="top: 50%;right:0;position: fixed;height: 100px;
background: #d3d3d33b;">
<v-icon style="top: calc(50% - 12px);position: relative;">mdi-chevron-right</v-icon>
</a>
</footer>
<v-bottom-sheet
v-model="showSettings"
:close-on-content-click="false"
max-width="500"
@keydown.esc.stop=""
scrollable
>
<v-card>
<v-toolbar dark color="primary">
<v-btn icon dark @click="showSettings = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ $t('bookreader.reader_settings') }}</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-0">
<v-list class="full-height full-width">
<v-subheader class="font-weight-black text-h6">{{ $t('bookreader.settings.general') }}</v-subheader>
<v-list-item>
<settings-switch v-model="alwaysFullscreen" :label="$t('bookreader.settings.always_fullscreen')"
:disabled="!screenfull.isEnabled"/>
</v-list-item>
<v-subheader class="font-weight-black text-h6">{{ $t('bookreader.settings.display') }}</v-subheader>
<v-list-item>
<v-list-item-title>{{ $t('epubreader.settings.viewing_theme') }}</v-list-item-title>
<v-btn
v-for="(a, i) in appearances"
:key="i"
:value="a.value"
:color="a.color"
:class="a.class"
class="mx-1"
@click="appearance = a.value"
>
<v-icon v-if="appearance === a.value">mdi-check</v-icon>
</v-btn>
</v-list-item>
<v-list-item>
<v-list-item-title>{{ $t('epubreader.settings.layout') }}</v-list-item-title>
<v-btn-toggle mandatory v-model="verticalScroll" class="py-3">
<v-btn :value="true">{{ $t('epubreader.settings.layout_scroll') }}</v-btn>
<v-btn :value="false">{{ $t('epubreader.settings.layout_paginated') }}</v-btn>
</v-btn-toggle>
</v-list-item>
<v-list-item v-if="!verticalScroll">
<v-list-item-title>{{ $t('epubreader.settings.column_count') }}</v-list-item-title>
<v-btn-toggle mandatory v-model="columnCount" class="py-3">
<v-btn v-for="(c, i) in columnCounts" :key="i" :value="c.value">{{ c.text }}</v-btn>
</v-btn-toggle>
</v-list-item>
<v-list-item class="justify-center">
<v-btn depressed @click="fontSize-=10">
<v-icon small>mdi-format-title</v-icon>
</v-btn>
<span class="caption mx-8" style="width: 2rem">{{ fontSize }}%</span>
<v-btn depressed @click="fontSize+=10">
<v-icon>mdi-format-title</v-icon>
</v-btn>
</v-list-item>
<v-list-item class="justify-center">
<v-btn depressed @click="lineHeight-=.1">
<v-icon>$formatLineSpacingDown</v-icon>
</v-btn>
<span class="caption mx-8" style="width: 2rem">{{ Math.round(lineHeight * 100) }}%</span>
<v-btn depressed @click="lineHeight+=.1">
<v-icon>mdi-format-line-spacing</v-icon>
</v-btn>
</v-list-item>
<v-list-item>
<v-slider
v-model="pageMargins"
:label="$t('epubreader.settings.page_margins')"
min="0.5"
max="4"
step="0.25"
ticks="always"
tick-size="3"
/>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-bottom-sheet>
<v-snackbar
v-model="notification.enabled"
centered
:timeout="notification.timeout"
>
<p class="text-h6 text-center ma-0">
{{ notification.message }}
</p>
</v-snackbar>
<shortcut-help-dialog
v-model="showHelp"
:shortcuts="shortcutsHelp"
/>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import D2Reader, {Locator} from '@d-i-t-a/reader'
import {bookManifestUrl} from '@/functions/urls'
import {BookDto} from '@/types/komga-books'
import {getBookTitleCompact} from '@/functions/book-title'
import {SeriesDto} from '@/types/komga-series'
import {Context, ContextOrigin} from '@/types/context'
import SettingsSwitch from '@/components/SettingsSwitch.vue'
import SettingsSelect from '@/components/SettingsSelect.vue'
import {TocEntry} from '@/types/epub'
import TocList from '@/components/TocList.vue'
import {Locations} from '@d-i-t-a/reader/dist/types/model/Locator'
import {epubShortcutsMenus, epubShortcutsSettings, shortcutsD2Reader} from '@/functions/shortcuts/epubreader'
import {flattenToc} from '@/functions/toc'
import ShortcutHelpDialog from '@/components/dialogs/ShortcutHelpDialog.vue'
import screenfull from 'screenfull'
import {getBookReadRouteFromMediaProfile} from '@/functions/book-format'
export default Vue.extend({
name: 'EpubReader',
components: {ShortcutHelpDialog, TocList, SettingsSelect, SettingsSwitch},
data: function () {
return {
screenfull,
fullscreenIcon: 'mdi-fullscreen',
d2Reader: {} as D2Reader,
book: undefined as unknown as BookDto,
series: undefined as unknown as SeriesDto,
siblingPrevious: {} as BookDto,
siblingNext: {} as BookDto,
incognito: false,
context: {} as Context,
contextName: '',
showSettings: false,
showToolbars: false,
showToc: false,
showHelp: false,
shortcuts: {} as any,
appearances: [
{
text: this.$t('enums.epubreader.appearances.day').toString(),
value: 'readium-default-on',
color: 'white',
class: 'black--text',
},
{
text: this.$t('enums.epubreader.appearances.sepia').toString(),
value: 'readium-sepia-on',
color: '#faf4e8',
class: 'black--text',
},
{
text: this.$t('enums.epubreader.appearances.night').toString(),
value: 'readium-night-on',
color: 'black',
class: 'white--text',
},
],
columnCounts: [
{text: this.$t('enums.epubreader.column_count.auto').toString(), value: 'auto'},
{text: this.$t('enums.epubreader.column_count.one').toString(), value: '1'},
{text: this.$t('enums.epubreader.column_count.two').toString(), value: '2'},
],
settings: {
appearance: 'readium-default-on',
pageMargins: 1,
lineHeight: 1,
fontSize: 100,
verticalScroll: false,
columnCount: 'auto',
alwaysFullscreen: false,
},
tocs: {
toc: undefined as unknown as TocEntry[],
landmarks: undefined as unknown as TocEntry[],
pageList: undefined as unknown as TocEntry[],
},
currentLocation: undefined as unknown as Locator,
historyCanGoBack: false,
historyCanGoForward: false,
notification: {
enabled: false,
message: '',
timeout: 4000,
},
clickTimer: undefined,
forceUpdate: false,
}
},
created() {
this.$vuetify.rtl = false
this.shortcuts = this.$_.keyBy([...epubShortcutsSettings, ...epubShortcutsMenus], x => x.key)
if (screenfull.isEnabled) screenfull.on('change', this.fullscreenChanged)
},
beforeDestroy() {
this.d2Reader.stop()
},
destroyed() {
this.$vuetify.rtl = (this.$t('common.locale_rtl') === 'true')
if (screenfull.isEnabled) {
screenfull.off('change', this.fullscreenChanged)
screenfull.exit()
}
},
mounted() {
Object.assign(this.settings, this.$store.state.persistedState.epubreader)
this.settings.alwaysFullscreen = this.$store.state.persistedState.webreader.alwaysFullscreen
this.setup(this.bookId)
},
props: {
bookId: {
type: String,
required: true,
},
},
beforeRouteUpdate(to, from, next) {
if (to.params.bookId !== from.params.bookId) {
// route update means either:
// - going to previous/next book, in this case the query.page is not set, so it will default to first page
// - pressing the back button of the browser and navigating to the previous book, in this case the query.page is set, so we honor it
this.d2Reader.stop()
this.setup(to.params.bookId, Number(to.query.page))
}
next()
},
computed: {
shortcutsHelp(): object {
return {
[this.$t('bookreader.shortcuts.reader_navigation').toString()]: [...shortcutsD2Reader],
[this.$t('bookreader.shortcuts.settings').toString()]: [...epubShortcutsSettings],
[this.$t('bookreader.shortcuts.menus').toString()]: epubShortcutsMenus,
}
},
tableOfContents(): TocEntry[] {
if (this.tocs.toc) return flattenToc(this.tocs.toc, 1, 0, this.currentLocation?.href)
return []
},
landmarks(): TocEntry[] {
if (this.tocs.landmarks) return flattenToc(this.tocs.landmarks, 1, 0, this.currentLocation?.href)
return []
},
pageList(): TocEntry[] {
if (this.tocs.pageList) return flattenToc(this.tocs.pageList, 1, 0, this.currentLocation?.href)
return []
},
hasToc(): boolean {
return this.tocs.toc?.length > 0
},
hasLandmarks(): boolean {
return this.tocs.landmarks?.length > 0
},
hasPageList(): boolean {
return this.tocs.pageList?.length > 0
},
bookTitle(): string {
if (!!this.book && !!this.series)
return getBookTitleCompact(this.book.metadata.title, this.series.metadata.title)
return this.book?.metadata?.title
},
appearance: {
get: function (): string {
return this.settings.appearance
},
set: function (color: string): void {
if (this.appearances.map(x => x.value).includes(color)) {
this.settings.appearance = color
this.d2Reader.applyUserSettings({appearance: color})
this.$store.commit('setEpubreaderSettings', this.settings)
}
},
},
verticalScroll: {
get: function (): boolean {
return this.settings.verticalScroll
},
set: function (value: string): void {
this.settings.verticalScroll = value
this.d2Reader.applyUserSettings({verticalScroll: value})
this.$store.commit('setEpubreaderSettings', this.settings)
},
},
columnCount: {
get: function (): boolean {
return this.settings.columnCount
},
set: function (value: string): void {
if (this.columnCounts.map(x => x.value).includes(value)) {
this.settings.columnCount = value
this.d2Reader.applyUserSettings({columnCount: value})
this.$store.commit('setEpubreaderSettings', this.settings)
}
},
},
pageMargins: {
get: function (): number {
return this.settings.pageMargins
},
set: function (value: number): void {
this.settings.pageMargins = value
this.d2Reader.applyUserSettings({pageMargins: value})
this.$store.commit('setEpubreaderSettings', this.settings)
},
},
lineHeight: {
get: function (): number {
return this.settings.lineHeight
},
set: function (value: number): void {
this.settings.lineHeight = value
this.d2Reader.applyUserSettings({lineHeight: value})
this.$store.commit('setEpubreaderSettings', this.settings)
},
},
fontSize: {
get: function (): number {
return this.settings.fontSize
},
set: function (value: number): void {
this.settings.fontSize = value
this.d2Reader.applyUserSettings({fontSize: value})
this.$store.commit('setEpubreaderSettings', this.settings)
},
},
alwaysFullscreen: {
get: function (): boolean {
return this.settings.alwaysFullscreen
},
set: function (alwaysFullscreen: boolean): void {
this.settings.alwaysFullscreen = alwaysFullscreen
this.$store.commit('setWebreaderAlwaysFullscreen', alwaysFullscreen)
if (alwaysFullscreen) this.enterFullscreen()
else screenfull.isEnabled && screenfull.exit()
},
},
},
methods: {
previousBook() {
if (!this.$_.isEmpty(this.siblingPrevious)) {
this.jumpToPreviousBook = false
this.$router.push({
name: getBookReadRouteFromMediaProfile(this.siblingPrevious.media.mediaProfile),
params: {bookId: this.siblingPrevious.id.toString()},
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
})
}
},
nextBook() {
if (this.$_.isEmpty(this.siblingNext)) {
this.closeBook()
} else {
this.jumpToNextBook = false
this.$router.push({
name: getBookReadRouteFromMediaProfile(this.siblingNext.media.mediaProfile),
params: {bookId: this.siblingNext.id.toString()},
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
})
}
},
enterFullscreen() {
if (screenfull.isEnabled) screenfull.request(document.documentElement, {navigationUI: 'hide'})
},
switchFullscreen() {
if (screenfull.isEnabled) screenfull.isFullscreen ? screenfull.exit() : this.enterFullscreen()
},
fullscreenChanged() {
if (screenfull.isEnabled && screenfull.isFullscreen) this.fullscreenIcon = 'mdi-fullscreen-exit'
else this.fullscreenIcon = 'mdi-fullscreen'
},
toggleToolbars() {
this.showToolbars = !this.showToolbars
},
toggleSettings() {
this.showSettings = !this.showSettings
},
toggleTableOfContents() {
this.showToc = !this.showToc
},
toggleHelp() {
this.showHelp = !this.showHelp
},
keyPressed(e: KeyboardEvent) {
this.shortcuts[e.key]?.execute(this)
},
clickThrough(e: MouseEvent) {
if (e.detail === 1) {
this.clickTimer = setTimeout(() => {
this.toggleToolbars()
}, 200)
}
if (e.detail === 2) {
clearTimeout(this.clickTimer)
}
},
async setup(bookId: string) {
this.book = await this.$komgaBooks.getBook(bookId)
this.series = await this.$komgaSeries.getOneSeries(this.book.seriesId)
// parse query params to get context and contextId
if (this.$route.query.contextId && this.$route.query.context
&& Object.values(ContextOrigin).includes(this.$route.query.context as ContextOrigin)) {
this.context = {
origin: this.$route.query.context as ContextOrigin,
id: this.$route.query.contextId as string,
}
this.book.context = this.context
this.contextName = (await (this.$komgaReadLists.getOneReadList(this.context.id))).name
document.title = `Komga - ${this.contextName} - ${this.book.metadata.title}`
} else {
document.title = `Komga - ${getBookTitleCompact(this.book.metadata.title, this.series.metadata.title)}`
}
// parse query params to get incognito mode
this.incognito = !!(this.$route.query.incognito && this.$route.query.incognito.toString().toLowerCase() === 'true')
this.d2Reader = await D2Reader.load({
url: new URL(bookManifestUrl(this.bookId)),
userSettings: this.settings,
storageType: 'memory',
injectables: [
// webpack will process the new URL (https://webpack.js.org/guides/asset-modules/#url-assets)
// we use a different extension so that the css-loader rule is not used (see vue.config.js)
{
type: 'style',
url: new URL('../styles/readium/ReadiumCSS-before.css.resource', import.meta.url).toString(),
r2before: true,
},
{
type: 'style',
url: new URL('../styles/readium/ReadiumCSS-default.css.resource', import.meta.url).toString(),
r2default: true,
},
{
type: 'style',
url: new URL('../styles/readium/ReadiumCSS-after.css.resource', import.meta.url).toString(),
r2after: true,
},
{type: 'style', url: new URL('../styles/r2d2bc/popup.css.resource', import.meta.url).toString()},
{type: 'style', url: new URL('../styles/r2d2bc/popover.css.resource', import.meta.url).toString()},
{type: 'style', url: new URL('../styles/r2d2bc/style.css.resource', import.meta.url).toString()},
],
requestConfig: {
credentials: 'include',
},
attributes: {
margin: 0, // subtract this from the iframe height, when setting the iframe minimum height
navHeight: 10, // used for positioning the toolbox
iframePaddingTop: 20, // top padding inside iframe
bottomInfoHeight: 35, // #reader-info-bottom height
},
rights: {
enableBookmarks: false,
enableAnnotations: false,
enableTTS: false,
enableSearch: false,
enableTimeline: false,
enableDefinitions: false,
enableContentProtection: false,
enableMediaOverlays: false,
enablePageBreaks: true,
autoGeneratePositions: false,
enableLineFocus: false,
customKeyboardEvents: false,
enableHistory: true,
enableCitations: false,
enableConsumption: false,
},
api: {
updateCurrentLocation: this.updateCurrentLocation,
keydownFallthrough: this.keyPressed,
clickThrough: this.clickThrough,
},
})
this.tocs.toc = this.d2Reader.tableOfContents
this.tocs.landmarks = this.d2Reader.landmarks
this.tocs.pageList = this.d2Reader.pageList
if (this.alwaysFullscreen) this.enterFullscreen()
try {
if (this?.context.origin === ContextOrigin.READLIST) {
this.siblingNext = await this.$komgaReadLists.getBookSiblingNext(this.context.id, bookId)
} else {
this.siblingNext = await this.$komgaBooks.getBookSiblingNext(bookId)
}
} catch (e) {
this.siblingNext = {} as BookDto
}
try {
if (this?.context.origin === ContextOrigin.READLIST) {
this.siblingPrevious = await this.$komgaReadLists.getBookSiblingPrevious(this.context.id, bookId)
} else {
this.siblingPrevious = await this.$komgaBooks.getBookSiblingPrevious(bookId)
}
} catch (e) {
this.siblingPrevious = {} as BookDto
}
},
historyBack() {
this.d2Reader.historyBack()
},
historyForward() {
this.d2Reader.historyForward()
},
updateCurrentLocation(location: Locator): Promise<Locator> {
// handle history
this.historyCanGoBack = this.d2Reader.historyCurrentIndex > 0
this.historyCanGoForward = this.d2Reader.historyCurrentIndex < this.d2Reader.history?.length - 1
this.currentLocation = location
return new Promise(function (resolve, _) {
resolve(location)
})
},
appearanceClass(suffix?: string): string {
let c = this.appearance.replace('readium-', '').replace('-on', '').replace('default', 'day')
if (suffix) c += `-${suffix}`
return c
},
goToEntry(tocEntry: TocEntry) {
if (tocEntry.href !== undefined) {
const url = new URL(tocEntry.href)
let locations = {
progression: 0,
} as Locations
let href = tocEntry.href
if (url.hash) {
locations = {
fragment: url.hash.slice(1),
}
href = tocEntry.href.substring(0, tocEntry.href.indexOf('#'))
}
let locator = {
href: href,
locations: locations,
}
this.d2Reader.goTo(locator)
this.showToc = false
}
},
closeDialog() {
if (this.showToc) {
this.showToc = false
return
}
if (this.showSettings) {
this.showSettings = false
return
}
if (this.showToolbars) {
this.showToolbars = false
return
}
this.closeBook()
},
closeBook() {
this.$router.push(
{
name: this.book.oneshot ? 'browse-oneshot' : 'browse-book',
params: {bookId: this.bookId.toString(), seriesId: this.book.seriesId},
query: {context: this.context.origin, contextId: this.context.id},
})
},
cycleViewingTheme() {
const i = (this.appearances.map(x => x.value).indexOf(this.settings.appearance) + 1) % this.appearances.length
const newValue = this.appearances[i]
this.appearance = newValue.value
const text = this.$t(newValue.text)
this.sendNotification(`${this.$t('epubreader.settings.viewing_theme')}: ${text}`)
},
changeLayout(scroll: boolean) {
this.verticalScroll = scroll
const text = scroll ? this.$t('epubreader.settings.layout_scroll') : this.$t('epubreader.settings.layout_paginated')
this.sendNotification(`${this.$t('epubreader.settings.layout')}: ${text}`)
},
cyclePagination() {
if (this.verticalScroll) {
this.columnCount = 'auto'
this.changeLayout(false)
} else {
const i = (this.columnCounts.map(x => x.value).indexOf(this.settings.columnCount) + 1) % this.columnCounts.length
const newValue = this.columnCounts[i]
this.columnCount = newValue.value
const text = this.$t(newValue.text)
this.sendNotification(`${this.$t('epubreader.settings.column_count')}: ${text}`)
}
},
changeFontSize(increase: boolean) {
this.fontSize += increase ? 10 : -10
},
sendNotification(message: string, timeout: number = 4000) {
this.notification.timeout = timeout
this.notification.message = message
this.notification.enabled = true
},
},
})
</script>
<style src="@d-i-t-a/reader/dist/reader.css"/>
<style scoped>
.settings {
z-index: 2;
}
.full-height {
height: 100%;
}
.full-width {
width: 100%;
}
.sepia-bg {
background-color: #faf4e8;
}
.sepia {
color: #faf4e8;
}
.day-bg {
background-color: #fff;
}
.day {
color: #fff;
}
.night-bg {
background-color: #000000;
}
.night {
color: #000000;
}
.scrolltab {
overflow-y: scroll;
}
.scrolltab-content {
max-height: calc(100vh - 48px);
}
</style>

View file

@ -0,0 +1,78 @@
import {TocEntry} from '@/types/epub'
import {flattenToc} from '@/functions/toc'
describe('Multiple levels', () => {
const initial = [
{title: '1'},
{
title: '2',
children: [
{title: '2.1'},
{
title: '2.2',
children: [
{
title: '2.2.1',
children: [
{title: '2.2.1.1'},
],
},
],
},
],
},
] as TocEntry[]
test('given toc when flattened then it should be flat with correct levels', () => {
const flattened = flattenToc(initial)
expect(flattened.length).toEqual(6)
expect(flattened[0].title).toEqual('1')
expect(flattened[0].level).toEqual(0)
expect(flattened[0].children).toEqual(undefined)
expect(flattened[1].title).toEqual('2')
expect(flattened[1].level).toEqual(0)
expect(flattened[1].children).toEqual(undefined)
expect(flattened[2].title).toEqual('2.1')
expect(flattened[2].level).toEqual(1)
expect(flattened[2].children).toEqual(undefined)
expect(flattened[3].title).toEqual('2.2')
expect(flattened[3].level).toEqual(1)
expect(flattened[3].children).toEqual(undefined)
expect(flattened[4].title).toEqual('2.2.1')
expect(flattened[4].level).toEqual(2)
expect(flattened[4].children).toEqual(undefined)
expect(flattened[5].title).toEqual('2.2.1.1')
expect(flattened[5].level).toEqual(3)
expect(flattened[5].children).toEqual(undefined)
})
test('given toc when flattened to a max of 2 then it should be flat with correct levels', () => {
const flattened = flattenToc(initial, 1)
expect(flattened.length).toEqual(2)
expect(flattened[0].title).toEqual('1')
expect(flattened[0].level).toEqual(0)
expect(flattened[0].children).toEqual(undefined)
expect(flattened[1].title).toEqual('2')
expect(flattened[1].level).toEqual(0)
expect(flattened[1].children?.length).toEqual(4)
expect(flattened[1].children!![0].title).toEqual('2.1')
expect(flattened[1].children!![0].level).toEqual(1)
expect(flattened[1].children!![1].title).toEqual('2.2')
expect(flattened[1].children!![1].level).toEqual(1)
expect(flattened[1].children!![2].title).toEqual('2.2.1')
expect(flattened[1].children!![2].level).toEqual(2)
expect(flattened[1].children!![3].title).toEqual('2.2.1.1')
expect(flattened[1].children!![3].level).toEqual(3)
})
})

View file

@ -12,4 +12,22 @@ module.exports = {
enableInSFC: false,
},
},
// custom rule for readium and r2d2bc css that needs to be made available, but untouched
configureWebpack: {
module: {
rules: [
{
test: [
/readium\/.*\.css.resource$/,
/r2d2bc\/.*\.css.resource$/,
],
type: 'asset/resource',
generator: {
filename: '[hash].css[query]',
},
},
],
},
},
}