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