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