From 653cd3d602cc429e995767d6accebb718050e161 Mon Sep 17 00:00:00 2001 From: khanhas Date: Mon, 1 Feb 2021 19:06:17 +1000 Subject: [PATCH] fix: remove hardcoded domain in list script add: Similars Books in popup UI --- backend/list.js | 2 +- backend/metadata.js | 20 +- qml/BookPopup.qml | 326 +++++++++++++-------- qml/Main.qml | 694 ++++++++++++++++++++++---------------------- store.cpp | 616 ++++++++++++++++++++------------------- store.h | 217 +++++++------- 6 files changed, 992 insertions(+), 883 deletions(-) diff --git a/backend/list.js b/backend/list.js index e6c0078..d0acbbd 100644 --- a/backend/list.js +++ b/backend/list.js @@ -52,7 +52,7 @@ fetch(listURL, fetchOptions).then(res => res.text()).then(html => { } return { - url: "https://b-ok.global" + $(ele).find('h3 a').attr('href'), + url: $(ele).find('h3 a').attr('href'), img: imageUrl, name: $(ele).find('h3').text().trim(), author: $(ele).find('div.authors').text().trim(), diff --git a/backend/metadata.js b/backend/metadata.js index ca78951..67705b5 100644 --- a/backend/metadata.js +++ b/backend/metadata.js @@ -1,12 +1,12 @@ const cheerio = require("cheerio"); -const { fetchOptions } = require("./common"); +const { domain, fetchOptions } = require("./common"); const fetch = require("node-fetch"); if (process.argv.length < 3) { console.error("No link"); return; } -fetch(process.argv[2], fetchOptions).then(a => a.text()).then(html => { +fetch(domain + process.argv[2], fetchOptions).then(a => a.text()).then(html => { const $ = cheerio.load(html, { _useHtmlParser2: true }); const author = $(`[itemprop="author"]`) @@ -30,17 +30,23 @@ fetch(process.argv[2], fetchOptions).then(a => a.text()).then(html => { description = detail; } - let downloadURL = $(".dlButton").attr("href"); - if (downloadURL === "#") { - downloadURL = ""; + let dlUrl = $(".dlButton").attr("href"); + if (dlUrl === "#") { + dlUrl = ""; } + const similars = $("#bMosaicBox .brick").get().map(a => ({ + url: $(a).find("a").attr("href"), + img: $(a).find("img").attr("src"), + })); + process.stdout.write(JSON.stringify({ name: $("h1").text().trim(), author, - imgFile: $(".details-book-cover").attr("href"), + img: $(".details-book-cover").attr("href"), description, - downloadURL, + dlUrl, + similars, })); }) .catch(err => { diff --git a/qml/BookPopup.qml b/qml/BookPopup.qml index 8a937b3..07a9ac6 100644 --- a/qml/BookPopup.qml +++ b/qml/BookPopup.qml @@ -1,120 +1,208 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.4 -import QtQuick.Layouts 1.0 - -Item { - property var model; - - property Popup popup: Popup { - id: bookPopup - width: 1000 - height: 1300 - x: (parent.width / 2) - (width / 2) - y: (parent.height / 2) - (height / 2) + 150 - closePolicy: Popup.CloseOnPressOutside - dim: true - Overlay.modeless: Rectangle { - color: "#505050f0" - MouseArea { - anchors.fill: parent - } - } - - Image { - id: bookImage - fillMode: Image.PreserveAspectFit - smooth: true - source: model ? model.imgFile : "" - width: 400 - height: 400 * 1.5 - x: parent.width / 2 - 400 / 2 - y: -(parent.height / 2 - height / 2) - ProgressBar { - visible: parent.progress < 1.0 - value: parent.progress - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: 80 - } - } - } - Text { - id: bookName - text: model ? model.name : "" - width: parent.width - 90 - x: 30 - anchors.top: bookImage.bottom - anchors.topMargin: 30 - font.family:"Maison Neue" - font.styleName: "Bold" - font.pixelSize: 40 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - } - - Text { - id: bookAuthor - text: model ? model.author : "" - width: bookName.width - x: bookName.x - anchors.top: bookName.bottom - anchors.topMargin: 20 - font.family:"Maison Neue" - font.styleName: "Medium" - font.pixelSize: 35 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - } - - Flickable { - anchors.top: bookAuthor.bottom - anchors.topMargin: 30 - anchors.leftMargin: 100 - anchors.rightMargin: 100 - x: 30 - width: bookPopup.width - 80 - height: parent.height - bookAuthor.y - bookAuthor.height - 110 - contentHeight: bookDesc.height - clip: true - flickableDirection: Flickable.VerticalFlick - boundsBehavior: Flickable.StopAtBounds - - Text { - id: bookDesc - textFormat: Text.RichText - text: model ? model.desc : "" - font.family:"EB Garamond" - font.styleName: "Medium" - font.pixelSize: 30 - width: parent.width - wrapMode: Text.Wrap - } - } - - Rectangle { - width: 300 - height: 80 - x: parent.width - 260 - y: parent.height - 30 - color: "black" - Text { - anchors.centerIn: parent - text: !model || !model.dlUrl ? "Unavailable" : model.status - font.family:"Maison Neue" - font.styleName: "Medium" - font.pixelSize: 30 - color: "white" - } - MouseArea { - anchors.fill: parent - onClicked: { - if(!model || !model.dlUrl || model.status !== "Download") { - return - } - model.download(); - } - } - } - } +import QtQuick 2.5 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +Item { + id: popupRoot + property var model; + property bool isBusy; + + property Popup popup: Popup { + id: bookPopup + width: 1000 + height: 1300 + x: (parent.width / 2) - (width / 2) + y: (parent.height / 2) - (height / 2) + 150 + closePolicy: Popup.CloseOnPressOutside + dim: true + padding: 40 + + Overlay.modeless: Rectangle { + color: "#90505050" + MouseArea { + anchors.fill: parent + } + } + + onOpened: bar.currentIndex = 0; + + contentChildren: [ + Image { + id: bookImage + fillMode: Image.PreserveAspectFit + smooth: true + source: model ? model.imgFile : "" + width: 400 + height: 400 * 1.5 + anchors.horizontalCenter: parent.horizontalCenter + y: -(parent.height / 2 - height / 2) + ProgressBar { + visible: parent.progress < 1.0 + value: parent.progress + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 80 + } + } + }, + Text { + id: bookName + text: model ? model.name : "" + anchors { + left: parent.left; right: parent.right + top: bookImage.bottom + topMargin: 30 + } + font.family:"Maison Neue" + font.styleName: "Bold" + font.pixelSize: 40 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + }, + Text { + id: bookAuthor + text: model ? model.author : "" + anchors { + left: parent.left; right: parent.right + top: bookName.bottom + topMargin: 20 + } + font.family:"Maison Neue" + font.styleName: "Medium" + font.pixelSize: 35 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + }, + TabBar { + id: bar + visible: !isBusy + anchors { + left: parent.left; right: parent.right + top: bookAuthor.bottom + topMargin: 30 + } + font.pixelSize: 25 + TabButton { + contentItem: Label { + text: "Details" + color: bar.currentIndex == 0 ? "black" : "gray" + font.underline: bar.currentIndex == 0 + horizontalAlignment: Text.AlignHCenter + } + background: Rectangle {} + } + TabButton { + contentItem: Label { + text: "Similar books" + color: bar.currentIndex == 1 ? "black" : "gray" + font.underline: bar.currentIndex == 1 + horizontalAlignment: Text.AlignHCenter + } + background: Rectangle {} + } + }, + StackLayout { + id: stack + anchors { + left: parent.left; right: parent.right + top: bar.bottom + topMargin: 30 + bottom: parent.bottom + bottomMargin: 30 + } + currentIndex: bar.currentIndex + Item { + Flickable { + anchors.fill: parent + contentHeight: bookDesc.height + clip: true + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + + Text { + id: bookDesc + textFormat: Text.RichText + text: model ? model.desc : "" + font.family:"EB Garamond" + font.styleName: "Medium" + font.pixelSize: 30 + width: parent.width + wrapMode: Text.Wrap + } + } + } + Item { + GridView { + id: recGrid + anchors.fill: parent + boundsBehavior: Flickable.StopAtBounds + cellHeight: stack.height / 2 + cellWidth: cellHeight / 1.5 + model: popupRoot.model ? popupRoot.model.similars : [] + flickableDirection: Flickable.HorizontalFlick + flow: GridView.TopToBottom + clip: true + snapMode: GridView.SnapToRow + flickDeceleration: 0 + delegate: Item { + id: itemRoot + width: recGrid.cellWidth + height: recGrid.cellHeight + Image { + id: image + fillMode: Image.PreserveAspectCrop + source: model.modelData.imgFile + anchors.fill: itemRoot + anchors.margins: 10 + } + MouseArea { + anchors.fill: itemRoot + onClicked: { + model.modelData.getDetail(popupRoot); + bar.currentIndex = 0; + popupRoot.model = model.modelData; + } + } + } + } + } + }, + Image { + z: 1 + source: "png/loading" + visible: isBusy + width: 60 + height: 60 + anchors.centerIn: parent + } + ] + + Rectangle { + id: download + visible: !isBusy + width: 300 + height: 80 + x: parent.width - 220 + y: parent.height + color: "black" + Text { + anchors.centerIn: parent + text: !model || !model.dlUrl ? "Unavailable" : model.status + font.family:"Maison Neue" + font.styleName: "Medium" + font.pixelSize: 30 + color: "white" + } + MouseArea { + anchors.fill: parent + onClicked: { + if(!model || !model.dlUrl || model.status !== "Download") { + return + } + model.download(); + } + } + } + } } \ No newline at end of file diff --git a/qml/Main.qml b/qml/Main.qml index 93225ad..20b4d4f 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -1,348 +1,346 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.4 -import QtQuick.Layouts 1.0 - -Rectangle { - id: canvas - width: 1404 - height: 1872 - readonly property int screenMargin: 40 - readonly property int columns: 4 - readonly property int rows: 3 - readonly property int itemPerPage: rows * columns - readonly property int bookWidth: (width - screenMargin * 2) / columns - readonly property int itemContentWidth: bookWidth - 20 - - Rectangle { - id: closeApp - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: screenMargin - width: 80 - height: 80 - radius: 40 - z: 1 - color: "black" - Text { - text: "x" - font.family: "Maison Neue" - font.bold: true - font.pixelSize:40 - color: "white" - anchors.centerIn: parent - anchors.topMargin: 10 - } - MouseArea { - anchors.fill: parent - onClicked: Qt.quit() - } - } - - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 40 - height: 100 - - Rectangle { - width: 250 - height: 80 - - Image { - source: "png/searchblack" - width: 80 - height: 80 - } - Text { - text: "Search" - font.pixelSize: 30 - - font.family:"Maison Neue" - font.styleName: "Demi" - verticalAlignment: Text.AlignVCenter - height: parent.height - x: 100 - } - MouseArea { - anchors.fill: parent - onClicked: queryUI.openSearch(true); - } - } - Rectangle { - width: accountStatusText.contentWidth + 60 - height: 60 - visible: store.accountStatus.length > 0 - border.color: "black" - border.width: 3 - radius: 30 - color: "white" - anchors { - right: parent.right - rightMargin: 120 - top: parent.top - topMargin: 10 - } - Text { - id: accountStatusText - text: store.accountStatus - color: "black" - font.pixelSize: 25 - font.family:"Maison Neue" - font.styleName: "Medium" - anchors.centerIn: parent - } - } - } - - GridView { - id: libView - objectName: "libView" - anchors.fill: parent - anchors.margins: screenMargin - anchors.topMargin: 140 - - boundsBehavior: Flickable.StopAtBounds - cellWidth: bookWidth - cellHeight: bookWidth * 1.5 + 30 - model: store.books - flickableDirection: Flickable.HorizontalFlick - flow: GridView.TopToBottom - clip: true - snapMode: GridView.SnapToRow - // pixelAligned: true - flickDeceleration: 0 - onMovementEnded: currentIndex = indexAt(contentX, 0) - - delegate: Item { - id: root - width: libView.cellWidth - height: libView.cellHeight - - Rectangle { - id: background - color: "white" - anchors.fill: parent - } - Text { - id: author - text: model.modelData.author - font.family:"Maison Neue" - font.styleName: "Demi" - font.pixelSize:25 - width: itemContentWidth - anchors.bottom: parent.bottom - anchors.bottomMargin: 10 - anchors.horizontalCenter: image.horizontalCenter - horizontalAlignment: Text.AlignHCenter - maximumLineCount: 1 - wrapMode: Text.Wrap - } - Text { - id: name - text: model.modelData.name - font.family:"Maison Neue" - font.styleName: "Bold" - font.pixelSize:25 - width: itemContentWidth - anchors.horizontalCenter: image.horizontalCenter - anchors.bottom: author.top - anchors.bottomMargin: 5 - horizontalAlignment: Text.AlignHCenter - maximumLineCount: 2 - wrapMode: Text.Wrap - } - Image { - id: image - fillMode: Image.PreserveAspectFit - height: name.y - 20 - source: model.modelData.imgFile - anchors.top: parent.top - anchors.topMargin: 10 - anchors.horizontalCenter: parent.horizontalCenter - } - - Rectangle { - id: downloaded - visible: model.modelData.status === "Downloaded" - anchors.top: parent.top - anchors.right: parent.right - width: 40 - height: 40 - color: "black" - - Text { - text: "↓" - color: "white" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family:"Maison Neue" - font.styleName: "Medium" - font.pixelSize:25 - } - } - - MouseArea { - anchors.fill: root - onPressed: { - background.color = "black" - name.color = "white" - author.color = "white" - } - onReleased: { - background.color = "white" - name.color = "black" - author.color = "black" - } - onCanceled: { - background.color = "white" - name.color = "black" - author.color = "black" - } - onClicked: { - model.modelData.getDetail(); - itemInfo.model = model.modelData; - itemInfo.popup.open(); - } - onPressAndHold: { - return; - } - } - } - } - - - Rectangle { - property bool isClickable: (libView.currentIndex + itemPerPage) < libView.count - id: goRight - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: screenMargin - width: 80; height: 80; radius: 40 - color: isClickable ? "black" : "gray" - Text { - text: "v" - font.family: "Maison Neue" - font.bold: true - font.pixelSize:40 - color: "white" - anchors.centerIn: parent - } - transform: Rotation { - origin.x: 40; - origin.y: 40; - angle: 270 - } - MouseArea { - anchors.fill: parent - onClicked: { - if (parent.isClickable) { - libView.currentIndex += itemPerPage; - } else { - return; - } - - if (libView.currentIndex >= libView.count) { - libView.currentIndex = libView.count - itemPerPage + libView.count % itemPerPage; - } - libView.positionViewAtIndex(libView.currentIndex, GridView.Beginning); - } - } - } - - - Rectangle { - property bool isClickable: (libView.currentIndex - itemPerPage) >= 0 || - (libView.currentIndex - rows) >= 0 - id: goLeft - anchors.bottom: parent.bottom - anchors.right: goRight.left - anchors.margins: screenMargin - width: 80; height: 80; radius: 40 - color: isClickable ? "black" : "gray" - Text { - text: "v" - font.family: "Maison Neue" - font.bold: true - font.pixelSize:40 - color: "white" - anchors.centerIn: parent - } - transform: Rotation { - origin.x: 40; - origin.y: 40; - angle: 90 - } - MouseArea { - anchors.fill: parent - onClicked: { - if (parent.isClickable) { - libView.currentIndex -= itemPerPage; - } else { - return; - } - - if (libView.currentIndex < 0) { - libView.currentIndex = 0 - } - libView.positionViewAtIndex(libView.currentIndex, GridView.Beginning); - } - } - } - - BookPopup { - id: itemInfo - anchors.fill: parent - } - - Query { - id: queryUI - anchors.fill: parent - z: 2 - storeFront: store - } - - Image { - z: 3 - source: "png/loading" - visible: store.isBusy - width: 100 - height: 100 - anchors.centerIn: parent - onVisibleChanged: { - if (!visible && queryUI.visible) { - queryUI.visible = false; - } - } - - Rectangle { - id: cancleButton - color: "black" - width: 200 - height: 70 - radius: 3 - anchors { - top: parent.bottom - topMargin: 40 - horizontalCenter: parent.horizontalCenter - } - Text { - text: "Cancel" - color: "white" - anchors.fill: parent - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family:"Maison Neue" - font.styleName: "Bold" - font.pixelSize: 30 - } - MouseArea { - anchors.fill: parent - onClicked: store.stopQuery() - } - } - } -} +import QtQuick 2.5 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.0 + +Rectangle { + id: canvas + width: 1404 + height: 1872 + readonly property int screenMargin: 40 + readonly property int columns: 4 + readonly property int rows: 3 + readonly property int itemPerPage: rows * columns + readonly property int bookWidth: (width - screenMargin * 2) / columns + readonly property int itemContentWidth: bookWidth - 20 + + Rectangle { + id: closeApp + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: screenMargin + width: 80 + height: 80 + radius: 40 + z: 1 + color: "black" + Text { + text: "x" + font.family: "Maison Neue" + font.bold: true + font.pixelSize:40 + color: "white" + anchors.centerIn: parent + anchors.topMargin: 10 + } + MouseArea { + anchors.fill: parent + onClicked: Qt.quit() + } + } + + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 40 + height: 100 + + Rectangle { + width: 250 + height: 80 + + Image { + source: "png/searchblack" + width: 80 + height: 80 + } + Text { + text: "Search" + font.pixelSize: 30 + + font.family:"Maison Neue" + font.styleName: "Demi" + verticalAlignment: Text.AlignVCenter + height: parent.height + x: 100 + } + MouseArea { + anchors.fill: parent + onClicked: queryUI.openSearch(true); + } + } + Rectangle { + width: accountStatusText.contentWidth + 60 + height: 60 + visible: store.accountStatus.length > 0 + border.color: "black" + border.width: 3 + radius: 30 + color: "white" + anchors { + right: parent.right + rightMargin: 120 + top: parent.top + topMargin: 10 + } + Text { + id: accountStatusText + text: store.accountStatus + color: "black" + font.pixelSize: 25 + font.family:"Maison Neue" + font.styleName: "Medium" + anchors.centerIn: parent + } + } + } + + GridView { + id: libView + objectName: "libView" + anchors.fill: parent + anchors.margins: screenMargin + anchors.topMargin: 140 + + boundsBehavior: Flickable.StopAtBounds + cellWidth: bookWidth + cellHeight: bookWidth * 1.5 + 30 + model: store.books + flickableDirection: Flickable.HorizontalFlick + flow: GridView.TopToBottom + clip: true + snapMode: GridView.SnapToRow + // pixelAligned: true + flickDeceleration: 0 + onMovementEnded: currentIndex = indexAt(contentX, 0) + + delegate: Item { + id: root + width: libView.cellWidth + height: libView.cellHeight + + Rectangle { + id: background + color: "white" + anchors.fill: parent + } + Text { + id: author + text: model.modelData.author + font.family:"Maison Neue" + font.styleName: "Demi" + font.pixelSize:25 + width: itemContentWidth + anchors.bottom: parent.bottom + anchors.bottomMargin: 10 + anchors.horizontalCenter: image.horizontalCenter + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 1 + wrapMode: Text.Wrap + } + Text { + id: name + text: model.modelData.name + font.family:"Maison Neue" + font.styleName: "Bold" + font.pixelSize:25 + width: itemContentWidth + anchors.horizontalCenter: image.horizontalCenter + anchors.bottom: author.top + anchors.bottomMargin: 5 + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + wrapMode: Text.Wrap + } + Image { + id: image + fillMode: Image.PreserveAspectFit + width: 200 + source: model.modelData.imgFile + anchors.centerIn: parent + } + + Rectangle { + id: downloaded + visible: model.modelData.status === "Downloaded" + anchors.top: parent.top + anchors.right: parent.right + width: 40 + height: 40 + color: "black" + + Text { + text: "↓" + color: "white" + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family:"Maison Neue" + font.styleName: "Medium" + font.pixelSize:25 + } + } + + MouseArea { + anchors.fill: root + onPressed: { + background.color = "black" + name.color = "white" + author.color = "white" + } + onReleased: { + background.color = "white" + name.color = "black" + author.color = "black" + } + onCanceled: { + background.color = "white" + name.color = "black" + author.color = "black" + } + onClicked: { + model.modelData.getDetail(itemInfo); + itemInfo.model = model.modelData; + itemInfo.popup.open(); + } + onPressAndHold: { + return; + } + } + } + } + + + Rectangle { + property bool isClickable: (libView.currentIndex + itemPerPage) < libView.count + id: goRight + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: screenMargin + width: 80; height: 80; radius: 40 + color: isClickable ? "black" : "gray" + Text { + text: "v" + font.family: "Maison Neue" + font.bold: true + font.pixelSize:40 + color: "white" + anchors.centerIn: parent + } + transform: Rotation { + origin.x: 40; + origin.y: 40; + angle: 270 + } + MouseArea { + anchors.fill: parent + onClicked: { + if (parent.isClickable) { + libView.currentIndex += itemPerPage; + } else { + return; + } + + if (libView.currentIndex >= libView.count) { + libView.currentIndex = libView.count - itemPerPage + libView.count % itemPerPage; + } + libView.positionViewAtIndex(libView.currentIndex, GridView.Beginning); + } + } + } + + + Rectangle { + property bool isClickable: (libView.currentIndex - itemPerPage) >= 0 || + (libView.currentIndex - rows) >= 0 + id: goLeft + anchors.bottom: parent.bottom + anchors.right: goRight.left + anchors.margins: screenMargin + width: 80; height: 80; radius: 40 + color: isClickable ? "black" : "gray" + Text { + text: "v" + font.family: "Maison Neue" + font.bold: true + font.pixelSize:40 + color: "white" + anchors.centerIn: parent + } + transform: Rotation { + origin.x: 40; + origin.y: 40; + angle: 90 + } + MouseArea { + anchors.fill: parent + onClicked: { + if (parent.isClickable) { + libView.currentIndex -= itemPerPage; + } else { + return; + } + + if (libView.currentIndex < 0) { + libView.currentIndex = 0 + } + libView.positionViewAtIndex(libView.currentIndex, GridView.Beginning); + } + } + } + + BookPopup { + id: itemInfo + anchors.fill: parent + } + + Query { + id: queryUI + anchors.fill: parent + z: 2 + storeFront: store + } + + Image { + z: 3 + source: "png/loading" + visible: store.isBusy + width: 100 + height: 100 + anchors.centerIn: parent + onVisibleChanged: { + if (!visible && queryUI.visible) { + queryUI.visible = false; + } + } + + Rectangle { + id: cancleButton + color: "black" + width: 200 + height: 70 + radius: 3 + anchors { + top: parent.bottom + topMargin: 40 + horizontalCenter: parent.horizontalCenter + } + Text { + text: "Cancel" + color: "white" + anchors.fill: parent + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family:"Maison Neue" + font.styleName: "Bold" + font.pixelSize: 30 + } + MouseArea { + anchors.fill: parent + onClicked: store.stopQuery() + } + } + } +} diff --git a/store.cpp b/store.cpp index b7bad71..6e6433b 100644 --- a/store.cpp +++ b/store.cpp @@ -1,302 +1,316 @@ -#include "store.h" - -Worker *infoThread = nullptr; -const QString nodePath("/opt/bin/node"); - -Store::Store() : rootView(rootObject()), context(rootContext()) -{ - worker = new Worker(nodePath, {}, true); - - if (loadConfig()) - { - newQuery(_exactMatch, _fromYear, _toYear, _language, _extension, _order, _query); - } - else - { - qDebug() << "config.json malformed"; - newQuery("0", "2021", "2021", "English", "epub", "Most Popular", ""); - } - - if (_cookieAvailable) { - infoThread = new Worker(nodePath, {QCoreApplication::applicationDirPath() + "/backend/info.js"}, true); - connect(infoThread, &Worker::readAll, this, [this](QByteArray bytes) { - QJsonParseError jsonError; - QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - qDebug() << "fromJson failed: " << jsonError.errorString(); - return; - } - if (!document.isObject()) - return; - - QJsonObject jsonObj = document.object(); - QString downloads = jsonObj.value("today_download").toString(""); - if (downloads.length() > 0) - { - auto counts = downloads.split("/"); - downloads.prepend("Downloads: "); - if (counts[0] == counts[1]) - { - downloads.prepend("⚠️ "); - } - this->setProperty("accountStatus", downloads); - } - }); - infoThread->start(); - } else { - setProperty("accountStatus", "⚠️ Cookie is not configured"); - } -} - -Store::~Store() -{ - if (worker != nullptr) - delete worker; - - if (infoThread != nullptr) - delete infoThread; -} - -void Store::newQuery(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query) -{ - QStringList args = { - QCoreApplication::applicationDirPath() + "/backend/list.js", - exactMatch, - fromYear, - toYear, - language, - extension, - order, - query, - }; - - stopQuery(); - - setProperty("isBusy", true); - - worker->args = args; - - connect(worker, &Worker::readAll, this, [this](QByteArray bytes) { - QJsonParseError jsonError; - QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - qDebug() << "fromJson failed: " << jsonError.errorString(); - return; - } - - if (!document.isArray()) - return; - - QList booksList; - QJsonArray list = document.array(); - for (auto book : list) - { - if (!book.isObject()) - continue; - - QJsonObject bookObj = book.toObject(); - Book *item = new Book(this->rootView); - item->setProperty("name", bookObj.value("name").toString()); - item->setProperty("author", bookObj.value("author").toString()); - item->setProperty("imgFile", bookObj.value("img").toString()); - item->setProperty("url", bookObj.value("url").toString()); - - booksList.push_back(item); - } - - setProperty("isBusy", false); - - for (auto oldBook : _books) { - delete oldBook; - } - - setProperty("books", QVariant::fromValue(booksList)); - }); - worker->start(); -} - -void Store::stopQuery() -{ - if (worker->isRunning()) - worker->terminate(); - setProperty("isBusy", false); -} - -bool Store::loadConfig() -{ - QFile file(QCoreApplication::applicationDirPath() + "/config.json"); - - if (!file.open(QIODevice::ReadOnly)) - return false; - - QByteArray bytes = file.readAll(); - file.close(); - - QJsonParseError jsonError; - QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - qDebug() << "fromJson failed: " << jsonError.errorString(); - return false; - } - if (!document.isObject()) - return false; - - QJsonObject jsonObj = document.object(); - - _cookieAvailable = jsonObj.value("cookie").toString("").length() > 0; - - QJsonValue defaultQueryValue = jsonObj.value("defaultQuery"); - if (!defaultQueryValue.isObject()) - return false; - - QJsonObject defaultQueryObj = defaultQueryValue.toObject(); - - _exactMatch = defaultQueryObj.value("exactMatch").toString(""); - _fromYear = defaultQueryObj.value("fromYear").toString(""); - _toYear = defaultQueryObj.value("toYear").toString(""); - _language = defaultQueryObj.value("language").toString(""); - _extension = defaultQueryObj.value("extension").toString(""); - _order = defaultQueryObj.value("order").toString(""); - _query = defaultQueryObj.value("query").toString(""); - - return true; -} - -bool Store::setConfig(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query) -{ - QFile file(QCoreApplication::applicationDirPath() + "/config.json"); - - if (!file.open(QIODevice::ReadOnly)) - { - qDebug() << "Can't open config.json in read-only"; - return false; - } - - QByteArray bytes = file.readAll(); - file.close(); - - QJsonParseError jsonError; - QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - qDebug() << "fromJson failed: " << jsonError.errorString(); - return false; - } - if (!document.isObject()) - { - qDebug() << "config.json malformed"; - return false; - } - - QJsonObject jsonObj = document.object(); - - QJsonObject defaultQuery; - - defaultQuery.insert("exactMatch", exactMatch); - defaultQuery.insert("fromYear", fromYear); - defaultQuery.insert("toYear", toYear); - defaultQuery.insert("language", language); - defaultQuery.insert("extension", extension); - defaultQuery.insert("order", order); - defaultQuery.insert("query", query); - - jsonObj.remove("defaultQuery"); - jsonObj.insert("defaultQuery", defaultQuery); - - file.remove(); - if (!file.open(QIODevice::WriteOnly)) - { - qDebug() << "Can't open config.json in write-only"; - return false; - } - - QByteArray writeBytes = QJsonDocument(jsonObj).toJson(QJsonDocument::Indented); - - QTextStream iStream(&file); - iStream.setCodec("utf-8"); - iStream << writeBytes; - file.close(); - - return true; -} - -Book::Book(QObject *parent) : QObject(parent) {} - -Book::~Book() { - if (worker != nullptr) { - delete worker; - } -} - -void Book::getDetail() -{ - if (_metadownloaded) - { - return; - } - - qDebug() << "Meta downloading"; - - QProcess proc; - proc.start(nodePath, QStringList{QCoreApplication::applicationDirPath() + "/backend/metadata.js", _url}, QIODevice::ReadOnly); - proc.waitForFinished(); - QByteArray bytes = proc.readAll(); - proc.close(); - - qDebug() << "Meta downloaded"; - - QJsonParseError jsonError; - QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - qDebug() << "fromJson failed: " << jsonError.errorString(); - setProperty("desc", bytes); - return; - } - if (!document.isObject()) - { - return; - } - QJsonObject detail = document.object(); - setProperty("dlUrl", detail.value("downloadURL").toString()); - setProperty("desc", detail.value("description").toString()); - setProperty("imgFile", detail.value("imgFile").toString()); - setProperty("status", "Download"); - _metadownloaded = true; -} - -void Book::download() -{ - if (worker == nullptr) { - worker = new Worker(nodePath, {QCoreApplication::applicationDirPath() + "/backend/download.js", _dlUrl}); - - connect(worker, &Worker::updateProgress, this, &Book::updateProgress); - connect(worker, &Worker::updateStatus, this, [this](QString stat) { - qDebug() << "LOG: " << stat; - if (stat.startsWith("ERR:")) - { - this->setProperty("status", QVariant(stat.trimmed())); - this->worker->terminate(); - } - }); - } - this->setProperty("status", QVariant("Downloading")); - worker->start(); -} - -void Book::updateProgress(int prog) -{ - if (prog == 100) - { - setProperty("status", QVariant("Downloaded")); - if (infoThread != nullptr && !infoThread->isRunning()) - { - infoThread->start(); - } - return; - } - setProperty("status", QVariant(QString::number(prog) + "%")); +#include "store.h" + +Worker *infoThread = nullptr; +const QString nodePath("/opt/bin/node"); + +Store::Store() : rootView(rootObject()), context(rootContext()) +{ + worker = new Worker(nodePath, {}, true); + + if (loadConfig()) + { + newQuery(_exactMatch, _fromYear, _toYear, _language, _extension, _order, _query); + } + else + { + qDebug() << "config.json malformed"; + newQuery("0", "2021", "2021", "English", "epub", "Most Popular", ""); + } + + if (_cookieAvailable) { + infoThread = new Worker(nodePath, {QCoreApplication::applicationDirPath() + "/backend/info.js"}, true); + connect(infoThread, &Worker::readAll, this, [this](QByteArray bytes) { + QJsonParseError jsonError; + QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "fromJson failed: " << jsonError.errorString(); + return; + } + if (!document.isObject()) + return; + + QJsonObject jsonObj = document.object(); + QString downloads = jsonObj.value("today_download").toString(""); + if (downloads.length() > 0) + { + auto counts = downloads.split("/"); + downloads.prepend("Downloads: "); + if (counts[0] == counts[1]) + { + downloads.prepend("⚠️ "); + } + this->setProperty("accountStatus", downloads); + } + }); + infoThread->start(); + } else { + setProperty("accountStatus", "⚠️ Cookie is not configured"); + } +} + +Store::~Store() +{ + if (worker != nullptr) + delete worker; + + if (infoThread != nullptr) + delete infoThread; +} + +void Store::newQuery(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query) +{ + QStringList args = { + QCoreApplication::applicationDirPath() + "/backend/list.js", + exactMatch, + fromYear, + toYear, + language, + extension, + order, + query, + }; + + stopQuery(); + + setProperty("isBusy", true); + + worker->args = args; + + connect(worker, &Worker::readAll, this, [this](QByteArray bytes) { + QJsonParseError jsonError; + QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "fromJson failed: " << jsonError.errorString(); + return; + } + + if (!document.isArray()) + return; + + QList booksList; + QJsonArray list = document.array(); + for (auto book : list) + { + if (!book.isObject()) + continue; + + QJsonObject bookObj = book.toObject(); + Book *item = new Book(); + item->setProperty("name", bookObj.value("name").toString()); + item->setProperty("author", bookObj.value("author").toString()); + item->setProperty("imgFile", bookObj.value("img").toString()); + item->setProperty("url", bookObj.value("url").toString()); + + booksList.push_back(item); + } + + setProperty("isBusy", false); + + for (auto oldBook : _books) { + delete oldBook; + } + + setProperty("books", QVariant::fromValue(booksList)); + }); + worker->start(); +} + +void Store::stopQuery() +{ + if (worker->isRunning()) + worker->terminate(); + setProperty("isBusy", false); +} + +bool Store::loadConfig() +{ + QFile file(QCoreApplication::applicationDirPath() + "/config.json"); + + if (!file.open(QIODevice::ReadOnly)) + return false; + + QByteArray bytes = file.readAll(); + file.close(); + + QJsonParseError jsonError; + QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "fromJson failed: " << jsonError.errorString(); + return false; + } + if (!document.isObject()) + return false; + + QJsonObject jsonObj = document.object(); + + _cookieAvailable = jsonObj.value("cookie").toString("").length() > 0; + + QJsonValue defaultQueryValue = jsonObj.value("defaultQuery"); + if (!defaultQueryValue.isObject()) + return false; + + QJsonObject defaultQueryObj = defaultQueryValue.toObject(); + + _exactMatch = defaultQueryObj.value("exactMatch").toString(""); + _fromYear = defaultQueryObj.value("fromYear").toString(""); + _toYear = defaultQueryObj.value("toYear").toString(""); + _language = defaultQueryObj.value("language").toString(""); + _extension = defaultQueryObj.value("extension").toString(""); + _order = defaultQueryObj.value("order").toString(""); + _query = defaultQueryObj.value("query").toString(""); + + return true; +} + +bool Store::setConfig(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query) +{ + QFile file(QCoreApplication::applicationDirPath() + "/config.json"); + + if (!file.open(QIODevice::ReadOnly)) + { + qDebug() << "Can't open config.json in read-only"; + return false; + } + + QByteArray bytes = file.readAll(); + file.close(); + + QJsonParseError jsonError; + QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "fromJson failed: " << jsonError.errorString(); + return false; + } + if (!document.isObject()) + { + qDebug() << "config.json malformed"; + return false; + } + + QJsonObject jsonObj = document.object(); + + QJsonObject defaultQuery; + + defaultQuery.insert("exactMatch", exactMatch); + defaultQuery.insert("fromYear", fromYear); + defaultQuery.insert("toYear", toYear); + defaultQuery.insert("language", language); + defaultQuery.insert("extension", extension); + defaultQuery.insert("order", order); + defaultQuery.insert("query", query); + + jsonObj.remove("defaultQuery"); + jsonObj.insert("defaultQuery", defaultQuery); + + file.remove(); + if (!file.open(QIODevice::WriteOnly)) + { + qDebug() << "Can't open config.json in write-only"; + return false; + } + + QByteArray writeBytes = QJsonDocument(jsonObj).toJson(QJsonDocument::Indented); + + QTextStream iStream(&file); + iStream.setCodec("utf-8"); + iStream << writeBytes; + file.close(); + + return true; +} + +Book::~Book() { + if (worker != nullptr) { + delete worker; + } +} + +void Book::getDetail(QObject* popup) +{ + if (_metadownloaded) + { + return; + } + + Worker *metaWorker = new Worker(nodePath, {QCoreApplication::applicationDirPath() + "/backend/metadata.js", _url}, true); + connect(metaWorker, &Worker::readAll, this, [this, metaWorker, popup](QByteArray bytes) { + QJsonParseError jsonError; + QJsonDocument document = QJsonDocument::fromJson(bytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "fromJson failed: " << jsonError.errorString(); + setProperty("desc", bytes); + return; + } + if (!document.isObject()) + { + return; + } + QJsonObject detail = document.object(); + setProperty("name", detail.value("name").toString()); + setProperty("author", detail.value("author").toString()); + setProperty("dlUrl", detail.value("dlUrl").toString()); + setProperty("desc", detail.value("description").toString()); + setProperty("imgFile", detail.value("img").toString()); + + QJsonArray similarsArray = detail.value("similars").toArray(); + QList recList; + for (auto recom : similarsArray) + { + QJsonObject bookObj = recom.toObject(); + Book *item = new Book(); + item->setProperty("imgFile", bookObj.value("img").toString()); + item->setProperty("url", bookObj.value("url").toString()); + recList.push_back(item); + } + setProperty("similars", QVariant::fromValue(recList)); + + setProperty("status", "Download"); + _metadownloaded = true; + popup->setProperty("isBusy", false); + + qDebug() << "Meta downloaded"; + delete metaWorker; + }); + + qDebug() << "Meta downloading"; + popup->setProperty("isBusy", true); + metaWorker->start(); +} + +void Book::download() +{ + if (worker == nullptr) { + worker = new Worker(nodePath, {QCoreApplication::applicationDirPath() + "/backend/download.js", _dlUrl}); + + connect(worker, &Worker::updateProgress, this, &Book::updateProgress); + connect(worker, &Worker::updateStatus, this, [this](QString stat) { + qDebug() << "LOG: " << stat; + if (stat.startsWith("ERR:")) + { + this->setProperty("status", QVariant(stat.trimmed())); + this->worker->terminate(); + } + }); + } + this->setProperty("status", QVariant("Downloading")); + worker->start(); +} + +void Book::updateProgress(int prog) +{ + if (prog == 100) + { + setProperty("status", QVariant("Downloaded")); + if (infoThread != nullptr && !infoThread->isRunning()) + { + infoThread->start(); + } + return; + } + setProperty("status", QVariant(QString::number(prog) + "%")); } \ No newline at end of file diff --git a/store.h b/store.h index b0c38b9..13cb1f8 100644 --- a/store.h +++ b/store.h @@ -1,108 +1,111 @@ -#ifndef STORE_H -#define STORE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "worker.h" -#include - - -class Book : public QObject -{ - Q_OBJECT - -public: - Book(QObject *parent); - ~Book(); - - Q_PROPERTY(QString imgFile MEMBER _imgFile NOTIFY imgFileChanged) - Q_PROPERTY(QString name MEMBER _name NOTIFY nameChanged) - Q_PROPERTY(QString author MEMBER _author NOTIFY authorChanged) - Q_PROPERTY(QString url MEMBER _url NOTIFY urlChanged) - Q_PROPERTY(QString desc MEMBER _desc NOTIFY descChanged) - Q_PROPERTY(QString dlUrl MEMBER _dlUrl NOTIFY dlUrlChanged) - Q_PROPERTY(QString status MEMBER _status NOTIFY statusChanged) - - Q_INVOKABLE void getDetail(); - Q_INVOKABLE void download(); - - void updateProgress(int prog); - -signals: - void imgFileChanged(QString); - void nameChanged(QString); - void authorChanged(QString); - void urlChanged(QString); - void descChanged(QString); - void dlUrlChanged(QString); - void statusChanged(QString); - -private: - Worker *worker = nullptr; - QString _imgFile; - QString _name; - QString _author; - QString _url; - QString _desc; - QString _dlUrl; - QString _status; - bool _metadownloaded = false; -}; - -class Store : public QQuickView -{ - Q_OBJECT -public: - Q_PROPERTY(QList books MEMBER _books NOTIFY booksChanged) - Q_PROPERTY(bool isBusy MEMBER _isBusy NOTIFY isBusyChanged) - Q_PROPERTY(QString exactMatch MEMBER _exactMatch) - Q_PROPERTY(QString fromYear MEMBER _fromYear) - Q_PROPERTY(QString toYear MEMBER _toYear) - Q_PROPERTY(QString language MEMBER _language) - Q_PROPERTY(QString extension MEMBER _extension) - Q_PROPERTY(QString order MEMBER _order) - Q_PROPERTY(QString query MEMBER _query) - Q_PROPERTY(QString accountStatus MEMBER _accountStatus NOTIFY accountStatusChanged) - - Store(); - ~Store(); - bool loadConfig(); - -public slots: - Q_INVOKABLE void newQuery(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query); - Q_INVOKABLE void stopQuery(); - Q_INVOKABLE bool setConfig(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query); - -signals: - void booksChanged(QList); - void isBusyChanged(bool); - void accountStatusChanged(QString); - -private: - QQuickItem *rootView; - QQmlContext *context; - QQuickItem *storeView; - QList _books; - bool _isBusy; - Worker *worker; - - QString _exactMatch; - QString _fromYear; - QString _toYear; - QString _language; - QString _extension; - QString _order; - QString _query; - QString _accountStatus; - - bool _cookieAvailable; -}; - +#ifndef STORE_H +#define STORE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "worker.h" +#include + + +class Book : public QObject +{ + Q_OBJECT + +public: + Book(){}; + ~Book(); + + Q_PROPERTY(QString imgFile MEMBER _imgFile NOTIFY imgFileChanged) + Q_PROPERTY(QString name MEMBER _name NOTIFY nameChanged) + Q_PROPERTY(QString author MEMBER _author NOTIFY authorChanged) + Q_PROPERTY(QString url MEMBER _url NOTIFY urlChanged) + Q_PROPERTY(QString desc MEMBER _desc NOTIFY descChanged) + Q_PROPERTY(QString dlUrl MEMBER _dlUrl NOTIFY dlUrlChanged) + Q_PROPERTY(QString status MEMBER _status NOTIFY statusChanged) + Q_PROPERTY(QList similars MEMBER _similars NOTIFY similarsChanged) + + Q_INVOKABLE void getDetail(QObject *popup); + Q_INVOKABLE void download(); + + void updateProgress(int prog); + +signals: + void imgFileChanged(QString); + void nameChanged(QString); + void authorChanged(QString); + void urlChanged(QString); + void descChanged(QString); + void dlUrlChanged(QString); + void statusChanged(QString); + void similarsChanged(QList); + +private: + Worker *worker = nullptr; + QString _imgFile; + QString _name; + QString _author; + QString _url; + QString _desc; + QString _dlUrl; + QString _status; + QList _similars; + bool _metadownloaded = false; +}; + +class Store : public QQuickView +{ + Q_OBJECT +public: + Q_PROPERTY(QList books MEMBER _books NOTIFY booksChanged) + Q_PROPERTY(bool isBusy MEMBER _isBusy NOTIFY isBusyChanged) + Q_PROPERTY(QString exactMatch MEMBER _exactMatch) + Q_PROPERTY(QString fromYear MEMBER _fromYear) + Q_PROPERTY(QString toYear MEMBER _toYear) + Q_PROPERTY(QString language MEMBER _language) + Q_PROPERTY(QString extension MEMBER _extension) + Q_PROPERTY(QString order MEMBER _order) + Q_PROPERTY(QString query MEMBER _query) + Q_PROPERTY(QString accountStatus MEMBER _accountStatus NOTIFY accountStatusChanged) + + Store(); + ~Store(); + bool loadConfig(); + +public slots: + Q_INVOKABLE void newQuery(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query); + Q_INVOKABLE void stopQuery(); + Q_INVOKABLE bool setConfig(QString exactMatch, QString fromYear, QString toYear, QString language, QString extension, QString order, QString query); + +signals: + void booksChanged(QList); + void isBusyChanged(bool); + void accountStatusChanged(QString); + +private: + QQuickItem *rootView; + QQmlContext *context; + QQuickItem *storeView; + QList _books; + bool _isBusy; + Worker *worker; + + QString _exactMatch; + QString _fromYear; + QString _toYear; + QString _language; + QString _extension; + QString _order; + QString _query; + QString _accountStatus; + + bool _cookieAvailable; +}; + #endif /* STORE_H */ \ No newline at end of file