Warning, /graphics/arianna/src/content/ui/EpubViewerPage.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
0002 // SPDX-License-Identifier: LGPL-2.1-only or LGPL-3.0-only or LicenseRef-KDE-Accepted-LGPL
0003
0004 import QtQuick 2.15
0005 import QtQuick.Controls 2.15 as QQC2
0006 import QtWebEngine 1.4
0007 import QtWebChannel 1.4
0008 import QtQuick.Layouts 1.15
0009 import org.kde.kirigami 2.19 as Kirigami
0010 import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
0011 import org.kde.arianna 1.0
0012 import Qt.labs.platform 1.1
0013
0014 Kirigami.Page {
0015 id: root
0016
0017 property string url
0018 property string filename
0019 property string locations
0020 property string currentLocation
0021 readonly property color readerTheme: Kirigami.Theme.backgroundColor
0022 readonly property bool hideSidebar: true
0023
0024 property var layouts: {
0025 'auto': {
0026 'renderTo': "'viewer'",
0027 'options': { width: '100%', flow: 'paginated', maxSpreadColumns: 2 }
0028 },
0029 'single': {
0030 renderTo: "'viewer'",
0031 options: { width: '100%', flow: 'paginated', spread: 'none' }
0032 },
0033 'scrolled': {
0034 renderTo: 'document.body',
0035 options: { width: '100%', flow: 'scrolled-doc' },
0036 },
0037 'continuous': {
0038 renderTo: 'document.body',
0039 options: { width: '100%', flow: 'scrolled', manager: 'continuous' },
0040 }
0041 }
0042
0043 signal relocated(newLocation: var, newProgress: int)
0044 signal locationsLoaded(locations: var)
0045 signal bookReady(title: var)
0046 signal bookClosed()
0047
0048 function reloadBook() {
0049 if (!root.url || view.loading) {
0050 return;
0051 }
0052 const renderTo = layouts['auto'].renderTo;
0053 const options = JSON.stringify(layouts['auto'].options);
0054 const urlNormalized = JSON.stringify(root.url).replace("#", "%23").replace("?", "%3F");
0055 view.runJavaScript(`open(${urlNormalized}, "filename.epub", "epub", ${renderTo}, '${options}')`);
0056 }
0057
0058 title: backend.metadata ? backend.metadata.title : ''
0059 padding: 0
0060
0061 Keys.onLeftPressed: view.prev()
0062 Keys.onRightPressed: view.next()
0063
0064 onUrlChanged: reloadBook()
0065 onReaderThemeChanged: backend.setStyle()
0066
0067 actions: [
0068 Kirigami.Action {
0069 displayComponent: KirigamiComponents.SearchPopupField {
0070 visible: view.file !== ''
0071
0072 implicitWidth: Kirigami.Units.gridUnit * 14
0073
0074 spaceAvailableRight: false
0075
0076 autoAccept: false
0077 onAccepted: if (text === '') {
0078 view.runJavaScript(`find.clearHighlight()`)
0079 } else {
0080 searchResultModel.search(text);
0081 searchResultModel.loading = true;
0082 }
0083
0084 popupContentItem: ListView {
0085 id: searchView
0086
0087 model: searchResultModel
0088
0089 delegate: QQC2.ItemDelegate {
0090 id: searchDelegate
0091
0092 required property string sectionMarkup
0093 required property string markup
0094 required property string cfi
0095
0096 width: ListView.view.width
0097
0098 leftInset: 1
0099 rightInset: 1
0100
0101 highlighted: activeFocus
0102
0103 onClicked: view.runJavaScript(`rendition.display('${cfi}')`)
0104
0105 Kirigami.Theme.colorSet: Kirigami.Theme.Window
0106 Kirigami.Theme.inherit: false
0107
0108 contentItem: ColumnLayout {
0109 QQC2.Label {
0110 Layout.fillWidth: true
0111 text: searchDelegate.sectionMarkup
0112 wrapMode: Text.WordWrap
0113 font: Kirigami.Theme.smallFont
0114 }
0115 QQC2.Label {
0116 Layout.fillWidth: true
0117 text: searchDelegate.markup
0118 wrapMode: Text.WordWrap
0119 }
0120 }
0121 }
0122
0123 Kirigami.PlaceholderMessage {
0124 text: i18n("No search results")
0125 visible: searchView.count === 0 && searchField.text.length > 2
0126 icon.name: "system-search"
0127 anchors.centerIn: parent
0128 width: parent.width - Kirigami.Units.gridUnit * 4
0129 }
0130
0131 Kirigami.PlaceholderMessage {
0132 text: i18n("Loading")
0133 visible: searchResultModel.loading
0134 anchors.centerIn: parent
0135 width: parent.width - Kirigami.Units.gridUnit * 4
0136 }
0137 }
0138 }
0139 },
0140 Kirigami.Action {
0141 text: i18n("Book Details")
0142 displayHint: Kirigami.DisplayHint.IconOnly
0143 icon.name: "documentinfo"
0144 enabled: backend.metadata
0145 onTriggered: {
0146 applicationWindow().pageStack.pushDialogLayer(Qt.resolvedUrl("./BookDetailsPage.qml"), {
0147 metadata: backend.metadata,
0148 })
0149 }
0150 }
0151 ]
0152
0153 SearchModel {
0154 id: searchResultModel
0155
0156 onSearchTriggered: (text) => {
0157 view.runJavaScript(`find.find('${text}', true, true)`)
0158 }
0159 }
0160
0161 Kirigami.PlaceholderMessage {
0162 anchors.centerIn: parent
0163 width: parent.width - Kirigami.Units.gridUnit * 4
0164 text: i18n("No book selected")
0165 visible: root.url === ''
0166 helpfulAction: Kirigami.Action {
0167 text: i18n("Open file")
0168 onTriggered: {
0169 const fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
0170 fileDialog.accepted.connect(() => {
0171 const file = fileDialog.file;
0172 if (!file) {
0173 return;
0174 }
0175 root.url = file;
0176 })
0177 fileDialog.open();
0178 }
0179 }
0180 }
0181
0182 Component {
0183 id: openFileDialog
0184
0185 FileDialog {
0186 id: root
0187 parentWindow: applicationWindow()
0188 title: i18n("Please choose a file")
0189 nameFilters: [i18nc("Name filter for EPUB files", "eBook files (*.epub *.cb* *.fb2 *.fb2zip)")]
0190 }
0191 }
0192
0193 Connections {
0194 target: applicationWindow().contextDrawer
0195 function onGoTo(cfi) {
0196 view.goTo(cfi);
0197 }
0198 }
0199
0200 WebEngineView {
0201 id: view
0202 anchors.fill: parent
0203 url: Qt.resolvedUrl("main.html")
0204 visible: root.url !== ''
0205 webChannel: channel
0206
0207 onVisibleChanged: if (!visible) {
0208 root.bookClosed();
0209 }
0210
0211 onJavaScriptConsoleMessage: (level, message, lineNumber, sourceID) => {
0212 console.error('WEB:', level, message, lineNumber, sourceID)
0213 }
0214 onLoadingChanged: reloadBook()
0215
0216 function next() {
0217 view.runJavaScript('rendition.next()');
0218 }
0219
0220 function prev() {
0221 view.runJavaScript('rendition.prev()');
0222 }
0223
0224 function goTo(cfi) {
0225 view.runJavaScript('rendition.display("' + cfi + '")');
0226 }
0227
0228 QQC2.Menu {
0229 id: selectionPopup
0230 Connections {
0231 target: backend
0232 function onSelectionChanged() {
0233 selectionPopup.popup()
0234 }
0235 }
0236
0237 QQC2.MenuItem {
0238 text: i18n("Copy")
0239 icon.name: 'edit-copy'
0240 onClicked: Clipboard.saveText(backend.selection.text)
0241 }
0242
0243 QQC2.MenuItem {
0244 text: i18n("Find")
0245 icon.name: 'search'
0246 onClicked: view.runJavaScript(`find.find('${backend.selection.text}', true, true)`);
0247 }
0248 }
0249 }
0250 footer: QQC2.ToolBar {
0251 visible: backend.locationsReady
0252 contentItem: RowLayout {
0253 QQC2.ToolButton {
0254 id: progressButton
0255 text: i18nc("Book reading progress", "%1%", Math.round(backend.progress * 100))
0256 onClicked: menu.popup(progressButton, 0, - menu.height)
0257
0258 Accessible.role: Accessible.ButtonMenu
0259
0260 property QQC2.Menu menu: QQC2.Menu {
0261 width: Kirigami.Units.gridUnit * 10
0262 height: Kirigami.Units.gridUnit * 15
0263 closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent
0264 contentItem: ColumnLayout {
0265 Kirigami.FormLayout {
0266 Layout.fillWidth: true
0267 QQC2.Label {
0268 Kirigami.FormData.label: i18n("Time left in chapter:")
0269 text: backend.location.timeInChapter ? Format.formatDuration(backend.location.timeInChapter) : i18n("Loading")
0270 }
0271 QQC2.Label {
0272 Kirigami.FormData.label: i18n("Time left in book:")
0273 text: backend.location.timeInBook ? Format.formatDuration(backend.location.timeInBook) : i18n("Loading")
0274 }
0275 }
0276 }
0277 }
0278 QQC2.ToolTip.text: text
0279 QQC2.ToolTip.visible: hovered
0280 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
0281 }
0282 QQC2.ToolButton {
0283 text: i18n("Previous Page")
0284 display: QQC2.AbstractButton.IconOnly
0285 icon.name: "arrow-left"
0286 onClicked: view.prev()
0287 QQC2.ToolTip.text: text
0288 QQC2.ToolTip.visible: hovered
0289 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
0290 }
0291 QQC2.Slider {
0292 padding: Kirigami.Units.smallSpacing
0293 value: backend.progress
0294 onValueChanged: backend.progress = value
0295 live: false
0296 Layout.fillWidth: true
0297 }
0298 QQC2.ToolButton {
0299 text: i18n("Next Page")
0300 icon.name: "arrow-right"
0301 onClicked: view.next()
0302 display: QQC2.AbstractButton.IconOnly
0303 QQC2.ToolTip.text: text
0304 QQC2.ToolTip.visible: hovered
0305 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
0306 }
0307 }
0308 }
0309
0310 QtObject {
0311 id: backend
0312 WebChannel.id: "backend"
0313 property var selection: null
0314 property double progress: 0
0315 property var location
0316 property var locations: root.locations
0317 property bool locationsReady: false
0318 property var metadata: null
0319 property var top: ({})
0320 property string file: root.url
0321 function get(script, callback) {
0322 return view.runJavaScript(`JSON.stringify(${script})`, callback)
0323 }
0324 onMetadataChanged: if (metadata) {
0325 view.runJavaScript('loadLocations()', () => {
0326 view.runJavaScript('render()')
0327 });
0328 }
0329 function dispatch(action) {
0330 switch (action.type) {
0331 case 'book-ready':
0332 searchResultModel.clear();
0333 searchResultModel.loading = false;
0334 get('book.package.metadata', metadata => {
0335 backend.metadata = JSON.parse(metadata);
0336 root.bookReady(backend.metadata.title);
0337 Database.addBook(backend.file, metadata);
0338 });
0339 get('book.navigation.toc', toc => {
0340 applicationWindow().contextDrawer.model.importFromJson(toc)
0341 });
0342 break;
0343 case 'rendition-ready':
0344 setStyle();
0345 view.runJavaScript('setupRendition()');
0346 if (currentLocation) {
0347 view.runJavaScript(`rendition.display('${currentLocation}')`)
0348 } else {
0349 view.runJavaScript(`rendition.display()`);
0350 }
0351 break;
0352 case 'locations-ready':
0353 backend.locationsReady = true;
0354 break;
0355 case 'locations-generated':
0356 backend.locationsReady = true;
0357 root.locationsLoaded(action.payload.locations)
0358 break;
0359 case 'book-error':
0360 console.error('Book error', action.payload);
0361 break;
0362 case 'selection':
0363 backend.selection = action.payload;
0364 break;
0365 case 'relocated':
0366 root.relocated(action.payload.start.cfi, action.payload.start.percentage * 100)
0367 backend.location = action.payload;
0368 break;
0369 case 'find-results':
0370 searchResultModel.resultFound(action.payload.q, action.payload.results);
0371 break;
0372 }
0373 }
0374
0375 function setStyle() {
0376 const getIbooksInternalTheme = bgColor => {
0377 const red = bgColor.r;
0378 const green = bgColor.g;
0379 const blue = bgColor.b;
0380 const l = 0.299 * red + 0.587 * green + 0.114 * blue;
0381 if (l < 0.3) return 'Night';
0382 else if (l < 0.7) return 'Gray';
0383 else if (red > green && green > blue) return 'Sepia';
0384 else return 'White';
0385 }
0386 const fontDesc = Config.defaultFont;
0387 const fontFamily = fontDesc.family
0388 const fontSizePt = fontDesc.pointSize
0389 const fontSize = fontDesc.pixelSize
0390 let fontWeight = 400
0391 const fontStyle = fontDesc.styleName
0392
0393 // unfortunately, it appears that WebKitGTK doesn't support font-stretch
0394 const fontStretch = [
0395 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
0396 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'
0397 ][fontDesc.stretch]
0398
0399 const style = {
0400 fontFamily: fontFamily,
0401 fontSize: fontSize,
0402 fontWeight: fontWeight,
0403 fontStyle: fontStyle,
0404 fontStretch: fontStretch,
0405 spacing: Config.spacing,
0406 margin: Config.margin,
0407 maxWidth: Config.maxWidth,
0408 usePublisherFont: Config.usePublisherFont,
0409 justify: Config.justify,
0410 hyphenate: Config.hyphenate,
0411 fgColor: Kirigami.Theme.textColor.toString(),
0412 bgColor: Kirigami.Theme.backgroundColor.toString(),
0413 linkColor: Kirigami.Theme.linkColor.toString(),
0414 selectionFgColor: Kirigami.Theme.highlightedTextColor.toString(),
0415 selectionBgColor: Kirigami.Theme.highlightColor.toString(),
0416 invert: Config.invert,
0417 brightness: Config.brightness,
0418 ibooksInternalTheme: getIbooksInternalTheme(Kirigami.Theme.backgroundColor)
0419 }
0420
0421 view.runJavaScript(`setStyle(${JSON.stringify(style)})`)
0422 }
0423 }
0424
0425 WebChannel {
0426 id: channel
0427 registeredObjects: [backend]
0428 }
0429 }
0430