Merge pull request #946 from kiwix/qtlibrary

Remove Vue.js, replaced by Qt in the library
This commit is contained in:
Kelson 2023-08-01 12:31:45 +02:00 committed by GitHub
commit be760126e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1856 additions and 13365 deletions

View File

@ -15,7 +15,6 @@ jobs:
fail-fast: false
matrix:
distro:
- ubuntu-kinetic
- ubuntu-jammy
- ubuntu-focal
steps:
@ -40,14 +39,6 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
if: matrix.distro == 'ubuntu-kinetic'
name: Build package for ubuntu-kinetic
id: build-ubuntu-kinetic
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
name: Build package for ubuntu-jammy

View File

@ -34,9 +34,17 @@ DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
src/contentmanagerdelegate.cpp \
src/contentmanagerheader.cpp \
src/contentmanagermodel.cpp \
src/contenttypefilter.cpp \
src/descriptionnode.cpp \
src/findinpagebar.cpp \
src/kiwixconfirmbox.cpp \
src/kiwixloader.cpp \
src/rownode.cpp \
src/suggestionlistworker.cpp \
src/thumbnaildownloader.cpp \
src/translation.cpp \
src/main.cpp \
src/mainwindow.cpp \
@ -67,9 +75,19 @@ SOURCES += \
src/static_content.cpp
HEADERS += \
src/contentmanagerdelegate.h \
src/contentmanagerheader.h \
src/contentmanagermodel.h \
src/contentmanagerview.h \
src/contenttypefilter.h \
src/descriptionnode.h \
src/findinpagebar.h \
src/kiwixconfirmbox.h \
src/kiwixloader.h \
src/node.h \
src/rownode.h \
src/suggestionlistworker.h \
src/thumbnaildownloader.h \
src/translation.h \
src/mainwindow.h \
src/kiwixapp.h \
@ -87,7 +105,6 @@ HEADERS += \
src/webpage.h \
src/about.h \
src/contentmanager.h \
src/contentmanagerview.h \
src/tabbar.h \
src/contentmanagerside.h \
src/readinglistbar.h \
@ -101,7 +118,9 @@ HEADERS += \
src/static_content.h
FORMS += \
src/contentmanagerview.ui \
src/findinpagebar.ui \
ui/kiwixconfirmbox.ui \
ui/mainwindow.ui \
ui/about.ui \
src/contentmanagerside.ui \

View File

@ -1,8 +1,5 @@
<RCC>
<qresource prefix="/">
<file>js/vue.js</file>
<file>texts/_contentManager.html</file>
<file>css/_contentManager.css</file>
<file>js/_contentManager.js</file>
</qresource>
</RCC>

View File

@ -1,271 +1,77 @@
html, body {
padding: 0;
margin: 0;
height: 100%;
position: relative;
width: 100%;
overflow: hidden;
}
#app {
height: 100%;
position: relative;
}
*:focus {
outline: none;
QTreeView::branch:open:has-children {
image: url(:/icons/caret-down-solid.svg);
padding: 6px;
}
#searchBar {
padding: 10px;
QTreeView::branch:closed:has-children {
image: url(:/icons/caret-right-solid.svg);
padding: 7px;
}
#searchInput {
background-image: url('qrc:///icons/search.svg');
background-repeat: no-repeat;
background-position: left 10px top 10px;
background-size: 23px 23px;
padding: 0;
margin: 0;
padding-left: 45px;
height: 40px;
width: 90%;
border: 1px solid #EEE;
QTreeView::item:has-children {
border-bottom: 1px solid #cccccc;
}
#bookTable {
position: relative;
height: calc(100% - 42px); /* 42px = 40px(height of #searchInput) + 2px(border) */
width: 100%;
QTreeView {
font-family: 'Selawik';
padding: 4px;
border: none;
}
#bookList {
height: calc(100% - 19px - 20px); /*19px the header size, 20px the header margin-top */
overflow-y:scroll;
overflow-x:hidden;
position: relative;
width: 100%
QTreeView::item:hover {
background-color: #eaecf0;
}
.tablerow,
.header {
display: flex;
flex-direction: row;
width: 100%;
}
.header {
color: #555;
margin-top: 20px;
}
.tablecell{
flex-basis:20%;
font-family: sans-serif;
QHeaderView::section {
color: #666666;
background-color: #fff;
border-width: 0px 0px 2px;
border-color: black;
border-style: plain;
font-size: 16px;
font-family: 'Selawik';
padding: 4px;
margin-left: 10px;
}
.sortable:hover {
cursor: pointer;
QHeaderView::down-arrow {
image: url(:/icons/caret-down-solid.svg);
height: 12px;
width: 12px;
padding-bottom: 6px;
subcontrol-position: left;
}
.sortableBold {
font-weight: bold;
QHeaderView::up-arrow {
image: url(:/icons/caret-up-solid.svg);
height: 12px;
width: 12px;
padding-bottom: 6px;
subcontrol-position: left;
}
i {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
QMenu {
background-color: white;
margin: 2px;
font-family: 'Selawik';
}
.arrowUp {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
QMenu::item {
padding: 2px 25px 2px 20px;
border-bottom: 1px solid #cccccc;
}
.arrowDown {
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
QMenu::item:selected {
background-color: #cccccc;
}
.cell0 {
flex-basis: 60px;
flex-grow: 0;
flex-shrink: 0;
}
.tablerow > .cell1 {
font-weight: bold;
}
.cell0 > img {
width: 24px;
height: 24px;
}
.cell1,
.cell2,
.cell3,
.cell4,
.cell5 {
flex-grow: 1;
margin: 0 10px;
}
.cell2,
.cell3,
.cell4 {
overflow: hidden;
}
.cell5 {
flex-basis: 30%;
flex-grow: 2;
flex-direction: row;
text-align: center;
}
summary {
height: 64px;
align-items: center;
}
.book {
border-top: 1px solid #EEE;
padding: 10px;
}
button {
background: transparent;
border: 0;
}
.tablerow button {
/* color: blue; */
color:#555;
/* font-weight: bold; */
font-size: 16px;
border: 0;
background: transparent;
border-radius: 2px;
}
.tablerow button::first-letter {
text-transform: uppercase;
}
.tablerow button:hover {
/* color: white;
background: blue; */
cursor: pointer;
font-weight: bold;
}
details:hover {
background-color: #d9e9ff;
cursor: pointer;
}
.menu {
/* width: 120px; */
box-shadow: 0 4px 5px 3px rgba(0, 0, 0, 0.2);
position: fixed;
display: none;
z-index: 99999999999999;
background-color: white;
}
.menu-options {
list-style: none;
padding: 0;
margin: 2px 0;
}
.menu-option {
font-weight: 500;
font-size: 14px;
padding: 10px 40px 10px 20px;
cursor: pointer;
}
.menu-option:hover {
background: rgba(0, 0, 0, 0.2);
}
.loader {
margin: 0 auto;
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
.do-not-display {
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
:root {
--circle-wrap-dimension: 40px;
--download: 0deg;
--inside-margin: 5px;
--inside-circle-dimension: 30px;
--border-radius: 50%;
}
.circle-wrap {
position: relative;
top: 10px;
display: inline-block;
width: var(--circle-wrap-dimension);
height: var(--circle-wrap-dimension);
background: #e6e2e7;
border-radius: var(--border-radius);
}
.circle-wrap .circle .mask,
.circle-wrap .circle .fill {
width: var(--circle-wrap-dimension);
height: var(--circle-wrap-dimension);
position: absolute;
border-radius: var(--border-radius);
}
.circle-wrap .circle .mask {
clip: rect(0, var(--circle-wrap-dimension), var(--circle-wrap-dimension), calc(var(--circle-wrap-dimension) / 2));
}
.circle-wrap .circle .mask .fill {
clip: rect(0, calc(var(--circle-wrap-dimension) / 2), var(--circle-wrap-dimension), 0);
background-color: #3498db;
}
.circle-wrap .circle .mask.full,
.circle-wrap .circle .fill {
transform: rotate(var(--download));
}
.circle-wrap .inside-circle {
width: var(--inside-circle-dimension);
height: var(--inside-circle-dimension);
border-radius: var(--border-radius);
background: #fff;
text-align: center;
margin-top: var(--inside-margin);
margin-left: var(--inside-margin);
position: absolute;
z-index: 100;
}
.circle-wrap img {
height: 30px;
width: auto;
}
.cancel-button {
position: relative;
top: 10px;
height: 40px;
width: auto;
}
.line {
display: inline;
QLineEdit {
font-family: 'Selawik';
padding: 4px;
border: none;
border-bottom: 1px solid #cccccc;
color: #666666;
font-size: 16px;
height: 32px;
line-height: 24px;
}

View File

@ -0,0 +1,32 @@
* {
font-family: 'Selawik';
background-color: white;
font-size: 16px;
}
QDialog {
border: 2px solid #cccccc;
}
#confirmTitle {
font-size: 18px;
line-height: 44px;
font-weight: bold;
}
QPushButton {
opacity: 1;
padding: 6px;
outline: 0;
border-radius: 2px;
width: 24px;
border: 1px solid #3366cc;
color: #3366cc;
}
QPushButton:hover {
background-color: #3366cc;
color: white;
}

View File

@ -122,6 +122,7 @@
"details":"Full article",
"yes":"yes",
"no":"no",
"ok":"ok",
"no-filter":"no filter",
"open-link-in-web-browser":"Open link in web browser",
"download-dir-dialog-title":"Are you sure you want to change the download directory?",
@ -135,5 +136,13 @@
"monitor-clear-dir-dialog-msg":"This will stop checking the monitor directory for new ZIM files.",
"monitor-directory-tooltip":"All ZIM files in this directory will be automatically added to the library.",
"next-tab":"Move to next tab",
"previous-tab":"Move to previous tab"
"previous-tab":"Move to previous tab",
"cancel-download": "Cancel Download",
"cancel-download-text": "Are you sure you want to cancel the download of <b>{{ZIM}}</b>?",
"delete-book": "Delete book",
"delete-book-text": "Are you sure you want to delete <b>{{ZIM}}</b>?",
"download-storage-error": "Storage Error",
"download-storage-error-text": "The system doesn't have enough storage available.",
"download-unavailable": "Download Unavailable",
"download-unavailable-text": "This download is unavailable."
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z"/></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"/></svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Layer_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256 1256"
style="enable-background:new 0 0 1256 1256;" xml:space="preserve">
<style type="text/css">
.st0{fill:#010101;}
</style>
<path class="st0" d="M1165,764.1c-8.3-36.4-68.5-141.3-191.6-234.4c-22.5-17.1-42.8-31.3-59.7-42.6
c24.6-105.3-103.3-232.3-228.1-172.5C596,230.3,496.1,195.9,404.2,197.3c-243.3,3.4-431,256.9-229.1,498.8c0.1,0.1,0.2,0.2,0.4,0.4
c3.1,3.7,6.3,7.4,9.5,11.1c13.1,15.7,21.8,29.6,29.2,54.1L274.4,959h-21.3c-19.6,0-35.6,15.9-35.6,35.6h80.8l135.8,64.2
c8.4-17.8,0.8-39-16.9-47.3l-35.6-16.8H484c0-19.6-15.9-35.6-35.6-35.6h-92.8c-16.2,0-30.6-10.6-35.3-26.1l-47.7-156.7
c-11.9-41.2,15.4-68.1,41.1-71.3c23.4-2.9,35.2,12.2,46.2,48.8l42.4,139h-21.3c-19.6,0-35.6,15.9-35.6,35.6h80.8l135.8,64.2
c8.4-17.8,0.8-39-16.9-47.3l-35.6-16.8h75.1c7.6,12.9,16.9,25.1,28,36.1c70,70,183.7,70,253.7,0s70-183.7,0-253.7s-183.7-70-253.7,0
c-49.2,49.2-63.9,120-43.9,182h-85c-16.2,0-30.6-10.6-35.3-26.1L378,635.4l12-6.4c167.1-70.1,345.8,55.1,470.2-65.2
c0.3-0.3,0.6-0.6,0.8-0.8c15.4-14,30.8-28.3,76.3,0.2c49,30.7,157.1,110.8,206.1,247.8C1143.5,811,1173.2,800.4,1165,764.1z
M821.2,460.6c-0.4-18.7-15.6-33.7-34.5-33.7c-19,0-34.5,15.4-34.5,34.5c0,10.4,4.6,19.6,11.8,25.9c-25-4.8-43.8-26.6-43.8-52.9
c0-29.8,24.1-53.9,53.9-53.9c29.8,0,53.9,24.1,53.9,53.9C828,443.9,825.5,452.8,821.2,460.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,264 +0,0 @@
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function niceBytes(x){
var unitIndex = 0;
var n = parseInt(x, 10) || 0;
while(n >= 1024 && ++unitIndex)
n = n/1024;
return(n.toFixed(n >= 10 || unitIndex < 1 ? 0 : 2) + ' ' + units[unitIndex]);
}
function getIndexById(id) {
var index = 0;
for(var i = 0; i < app.books.length; i++) {
if (app.books[i]["id"] == id) {
index = i;
break;
}
}
return index;
}
function setTranslations(translations) {
app.translations = createDict(TRANSLATION_KEYS, translations);
}
const BOOK_KEYS = ["id", "name", "path", "url", "size", "description", "title", "tags", "date", "faviconUrl", "faviconMimeType", "downloadId"];
function addBook(values) {
var b = createDict(BOOK_KEYS, values);
if (b.downloadId && !downloadUpdaters.hasOwnProperty(b.id)) {
downloadUpdaters[b.id] = setInterval(function() { getDownloadInfo(b.id); }, 1000);
}
app.books.push(b);
}
function onBooksChanged () {
app.books = [];
for(var i=0; i<contentManager.bookIds.length; i++) {
var id = contentManager.bookIds[i];
contentManager.getBookInfos(id, BOOK_KEYS, addBook);
}
app.displayedBooksNb = 20;
}
function onOneBookChanged (id) {
var index = getIndexById(id);
contentManager.getBookInfos(id, BOOK_KEYS, function(values) {
var b = createDict(BOOK_KEYS, values);
if (b.downloadId && !downloadUpdaters.hasOwnProperty(b.id)) {
downloadUpdaters[b.id] = setInterval(function() { getDownloadInfo(b.id); }, 1000);
}
app.books.splice(index, 1, b);
});
}
function onBookRemoved (id) {
var index = getIndexById(id);
app.books.splice(index, 1);
}
downloadUpdaters = {}
const DOWNLOAD_KEYS = ["id", "status", "followedBy", "path", "totalLength", "completedLength", "downloadSpeed", "verifiedLength"];
function getDownloadInfo(id) {
contentManager.updateDownloadInfos(id, DOWNLOAD_KEYS, function(values) {
if (values.length == 0) {
clearInterval(downloadUpdaters[id]);
return;
}
d = createDict(DOWNLOAD_KEYS, values);
if (d.status == "completed") {
clearInterval(downloadUpdaters[id]);
Vue.delete(app.downloads, id);
return;
} else if (d.status == "error") {
clearInterval(downloadUpdaters[id]);
Vue.delete(app.downloads, id);
alert("Error: download failed.");
contentManager.eraseBook(id);
return;
}
d["completedLengthInDegree"] = Math.trunc(d["completedLength"] * 180 / d["totalLength"]).toString() + "deg";
Vue.set(app.downloads, id, d);
});
}
function displayLoadIcon(display) {
if (display) {
document.getElementById("load-icon").classList.remove("do-not-display")
document.getElementById("bookList").classList.add("do-not-display");
} else {
document.getElementById("load-icon").classList.add("do-not-display");
document.getElementById("bookList").classList.remove("do-not-display")
}
}
const TRANSLATION_KEYS = ["search-files",
"title",
"size",
"date",
"content-type",
"reset-sort",
"open",
"delete",
"download",
"resume",
"pause",
"cancel"];
function init() {
new QWebChannel(qt.webChannelTransport, function(channel) {
contentManager = channel.objects.contentManager;
app = new Vue({
el: "#app",
data: {
contentManager: contentManager,
displayedBooksNb: 20,
books: [],
downloads: {},
activeSortType:"",
sortOrderAsc:true,
translations:{}
},
methods: {
gt : function(key) {
return this.translations[key];
},
openBook : function(book) {
contentManager.openBook(book.id, function() {});
},
downloadBook : function(book) {
contentManager.downloadBook(book.id, function(did) {
if (did.length == 0) {
alert("Error: this download is not available.");
return;
}
if (did == "storage_error") {
alert("not enough storage available.");
return;
}
book.downloadId = did;
downloadUpdaters[book.id] = setInterval(function() { getDownloadInfo(book.id); }, 1000);
});
},
eraseBook : function(book) {
if (confirm("Are you sure you want to delete '" + book.title + "' ?")) {
contentManager.eraseBook(book.id);
}
},
pauseResumeBook : function(book) {
if (app.downloads[book.id].status == 'active') {
contentManager.pauseBook(book.id);
} else if (app.downloads[book.id].status == 'paused') {
contentManager.resumeBook(book.id);
}
},
cancelBook : function(book) {
contentManager.pauseBook(book.id);
if (confirm("Are you sure you want to abort the download of '" + book.title + "' ?")) {
contentManager.cancelBook(book.id);
clearInterval(downloadUpdaters[book.id]);
Vue.delete(app.downloads, book.id);
} else {
contentManager.resumeBook(book.id);
}
},
displayedBooks : function(books, nb) {
var a = books.slice(0, nb);
return a;
},
getBookFromMousePosition : function() {
var elements = document.elementsFromPoint(mouseX, mouseY);
var bookId = null;
for(var i = 0; i < elements.length; i++) {
if (elements[i].localName == "summary" && elements[i].classList.contains("book-summary")) {
bookId = elements[i].id;
break;
}
}
var book = null;
for(var i = 0; i < app.books.length; i++) {
if (app.books[i]["id"] == bookId) {
book = app.books[i];
break;
}
}
return book;
},
sortBookBy : function(sortBy) {
if (this.activeSortType == sortBy && this.sortOrderAsc)
this.sortOrderAsc = false;
else {
this.activeSortType = sortBy;
this.sortOrderAsc = true;
}
contentManager.setSortBy(this.activeSortType, this.sortOrderAsc);
},
isActive: function (sortType) {
return (this.activeSortType == sortType)
},
isUpOrDown: function (sortType, sortOrderAsc) {
return (sortType == this.activeSortType && this.sortOrderAsc == sortOrderAsc);
},
resetSort: function () {
this.sortOrderAsc = true;
this.activeSortType = "";
contentManager.setSortBy("unsorted", this.sortOrderAsc);
},
niceBytes : niceBytes
}
});
contentManager.booksChanged.connect(onBooksChanged);
contentManager.oneBookChanged.connect(onOneBookChanged);
contentManager.bookRemoved.connect(onBookRemoved);
contentManager.pendingRequest.connect(displayLoadIcon);
contentManager.getTranslations(TRANSLATION_KEYS, setTranslations);
onBooksChanged();
displayLoadIcon(false);
});
}
futurCall = null;
function setSearch(value) {
clearTimeout(futurCall);
futurCall = setTimeout(function(){contentManager.setSearch(value)}, 100);
}
function scrolled(e) {
if (e.offsetHeight + e.scrollTop >= e.scrollHeight) {
app.displayedBooksNb = Math.min(app.displayedBooksNb+20, app.books.length);
}
}
window.addEventListener("click", e => {
if (menuVisible)
displayMenu(null);
});
var mouseX, mouseY = 0;
window.addEventListener("contextmenu", e => {
e.preventDefault();
mouseX = e.pageX;
mouseY = e.pageY;
setContextMenuPosition();
var book = app.getBookFromMousePosition();
displayMenu(book);
});
var menuVisible = false;
function displayMenu(book) {
var menu = document.getElementById("menu");
menu.style.display = (book) ? "block" : "none";
menuVisible = (book) ? true : false;
if (!book)
return;
var localElements = document.getElementsByClassName("local-option");
for(var i = 0; i < localElements.length; i++)
localElements[i].style.display = (book.path) ? "block" : "none";
document.getElementsByClassName("download-option")[0].style.display = (!book.path && !app.downloads[book.id]) ? "block" : "none";
document.getElementsByClassName("pause-option")[0].style.display = (app.downloads[book.id] && app.downloads[book.id].status == 'active') ? "block" : "none";
document.getElementsByClassName("resume-option")[0].style.display = (app.downloads[book.id] && app.downloads[book.id].status == 'paused') ? "block" : "none";
document.getElementsByClassName("cancel-option")[0].style.display = (app.downloads[book.id]) ? "block" : "none";
}
function setContextMenuPosition() {
var menu = document.getElementById("menu");
menu.style.left = `${mouseX}px`;
menu.style.top = `${mouseY}px`;
}

View File

@ -1,7 +0,0 @@
function createDict(keys, values) {
var d = {}
for(var i=0; i<keys.length; i++) {
d[keys[i]] = values[i];
}
return d;
}

File diff suppressed because it is too large Load Diff

View File

@ -54,8 +54,11 @@
<file>icons/pause-button.png</file>
<file>icons/cancel-button.png</file>
<file>icons/new-tab-icon.svg</file>
<file>icons/library-icon.svg</file>
<file>icons/open-file.svg</file>
<file>js/tools.js</file>
<file>icons/placeholder-icon.png</file>
<file>icons/caret-down-solid.svg</file>
<file>icons/caret-right-solid.svg</file>
<file>icons/caret-up-solid.svg</file>
<file>icons/kiwix-logo.svg</file>
</qresource>
</RCC>

View File

@ -3,5 +3,6 @@
<file>css/style.css</file>
<file>css/popup.css</file>
<file>css/localServer.css</file>
<file>css/confirmBox.css</file>
</qresource>
</RCC>

View File

@ -1,96 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<script src="qrc:///js/vue.js"></script>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script src="qrc:///js/_contentManager.js"></script>
<script src="qrc:///js/tools.js"></script>
<link rel="stylesheet" type="text/css" href="qrc:///css/_contentManager.css"/>
</head>
<body onload="init()">
<div id="app">
<div id="searchBar">
<form>
<input id="searchInput" type="text" :placeholder="gt('search-files')" oninput="setSearch(this.value)"/>
</form>
</div>
<div id="bookTable">
<div class="header">
<span class="tablecell cell0"></span>
<span v-on:click="sortBookBy('title')" :class="{ sortableBold: isActive('title') }" class="tablecell cell1 sortable"><i :class="{ arrowDown: isUpOrDown('title', true), arrowUp: isUpOrDown('title', false) }"></i> {{ gt("title") }}</span>
<span v-on:click="sortBookBy('size')" :class="{ sortableBold: isActive('size') }" class="tablecell cell2 sortable"><i :class="{ arrowDown: isUpOrDown('size', true), arrowUp: isUpOrDown('size', false) }"></i> {{ gt("size") }} </span>
<span v-on:click="sortBookBy('date')" :class="{ sortableBold: isActive('date') }" class="tablecell cell3 sortable"><i :class="{ arrowDown: isUpOrDown('date', true), arrowUp: isUpOrDown('date', false) }"></i> {{ gt("date") }} </span>
<span class="tablecell cell4"> {{ gt("content-type") }} </span>
<span class="tablecell cell5 tablerow">
<button v-on:click="resetSort()"> {{ gt("reset-sort") }} </button>
</span>
</div>
<div id="load-icon" class="loader"></div>
<div id="bookList" onscroll=scrolled(this)>
<div id="menu" class="menu">
<ul class="menu-options">
<li v-on:click="openBook(getBookFromMousePosition())" class="menu-option local-option">{{ gt("open") }}</li>
<li v-on:click="eraseBook(getBookFromMousePosition())" class="menu-option local-option">{{ gt("delete") }}</li>
<li v-on:click="downloadBook(getBookFromMousePosition())" class="menu-option download-option">{{ gt("download") }}</li>
<li v-on:click="resumeBook(getBookFromMousePosition())" class="menu-option resume-option">{{ gt("resume") }}</li>
<li v-on:click="pauseBook(getBookFromMousePosition())" class="menu-option pause-option">{{ gt("pause") }}</li>
<li v-on:click="cancelBook(getBookFromMousePosition())" class="menu-option cancel-option">{{ gt("cancel") }}</li>
</ul>
</div>
<details v-for="book in displayedBooks(books, displayedBooksNb)" class="book">
<summary v-bind:id="book.id" class="tablerow book-summary">
<span class="tablecell cell0">
<img v-if="book.faviconUrl" v-bind:src="'https://' + book.faviconUrl" />
<img v-else-if="book.faviconMimeType" v-bind:src="'zim://' + book.id + '.favicon.meta'" />
</span>
<span class="tablecell cell1">
{{ book.title }}
</span>
<span class="tablecell cell2">
{{ niceBytes(book.size) }}
</span>
<span class="tablecell cell3">
{{ book.date }}
</span>
<span class="tablecell cell4">
{{ book.tags }}
</span>
<span class="tablecell cell5">
<template v-if="downloads[book.id]" class="line">
<span>
{{ niceBytes(downloads[book.id].completedLength) }} / {{ niceBytes(downloads[book.id].totalLength) }}
</span>
</template>
<button v-if="book.path" v-on:click="openBook(book)" class="line">Open</button>
<button v-if="!book.path && !downloads[book.id]" v-on:click="downloadBook(book)" class="">Download</button>
<!-- round download progress bar -->
<template v-if="downloads[book.id]" class="line">
<div v-on:click.stop.prevent="pauseResumeBook(book)" v-bind:style="{'--download': downloads[book.id].completedLengthInDegree}" class="circle-wrap">
<div class="circle">
<div class="mask full">
<div class="fill"></div>
</div>
<div class="mask half">
<div class="fill"></div>
</div>
<div class="inside-circle">
<img v-if="downloads[book.id] && downloads[book.id].status == 'active'" src="qrc:///icons/pause-button.png">
<img v-if="downloads[book.id] && downloads[book.id].status == 'paused'" src="qrc:///icons/play-button.png">
</div>
</div>
</div>
</template>
<!-- end -->
<img class="cancel-button" v-if="downloads[book.id]" v-on:click.stop.prevent="cancelBook(book)" src="qrc:///icons/cancel-button.png">
</span>
</summary>
<p class="content">
{{ book.description }}
</p>
</details>
</div>
</div>
</div>
</body></html>

View File

@ -11,6 +11,17 @@
#include <QDir>
#include <QStorageInfo>
#include <QMessageBox>
#include "contentmanagermodel.h"
#include <zim/error.h>
#include <zim/item.h>
#include <QHeaderView>
#include "contentmanagerdelegate.h"
#include "node.h"
#include "rownode.h"
#include "descriptionnode.h"
#include "kiwixconfirmbox.h"
#include <QtConcurrent/QtConcurrentRun>
#include "contentmanagerheader.h"
ContentManager::ContentManager(Library* library, kiwix::Downloader* downloader, QObject *parent)
: QObject(parent),
@ -21,12 +32,109 @@ ContentManager::ContentManager(Library* library, kiwix::Downloader* downloader,
// mp_view will be passed to the tab who will take ownership,
// so, we don't need to delete it.
mp_view = new ContentManagerView();
mp_view->registerObject("contentManager", this);
mp_view->setHtml();
managerModel = new ContentManagerModel(this);
const auto booksList = getBooksList();
managerModel->setBooksData(booksList);
auto treeView = mp_view->getView();
treeView->setModel(managerModel);
treeView->show();
auto header = new ContentManagerHeader(Qt::Orientation::Horizontal, treeView);
treeView->setHeader(header);
header->setSectionResizeMode(0, QHeaderView::Fixed);
header->setSectionResizeMode(1, QHeaderView::Stretch);
header->setSectionResizeMode(2, QHeaderView::Fixed);
header->setSectionResizeMode(3, QHeaderView::Fixed);
header->setSectionResizeMode(4, QHeaderView::Fixed);
header->setDefaultAlignment(Qt::AlignLeft);
header->setStretchLastSection(false);
header->setSectionsClickable(true);
header->setHighlightSections(true);
treeView->setWordWrap(true);
treeView->resizeColumnToContents(4);
treeView->setColumnWidth(0, 80);
treeView->setColumnWidth(5, 120);
// TODO: set width for all columns based on viewport
setCurrentLanguage(QLocale().name().split("_").at(0));
connect(mp_library, &Library::booksChanged, this, [=]() {emit(this->booksChanged());});
connect(this, &ContentManager::filterParamsChanged, this, &ContentManager::updateLibrary);
connect(this, &ContentManager::booksChanged, this, [=]() {
const auto nBookList = getBooksList();
managerModel->setBooksData(nBookList);
managerModel->refreshIcons();
});
connect(&m_remoteLibraryManager, &OpdsRequestManager::requestReceived, this, &ContentManager::updateRemoteLibrary);
connect(mp_view->getView(), SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onCustomContextMenu(const QPoint &)));
connect(this, &ContentManager::pendingRequest, mp_view, &ContentManagerView::showLoader);
connect(treeView, &QTreeView::doubleClicked, this, &ContentManager::openBookWithIndex);
}
QList<QMap<QString, QVariant>> ContentManager::getBooksList()
{
const auto bookIds = getBookIds();
QList<QMap<QString, QVariant>> bookList;
QStringList keys = {"title", "tags", "date", "id", "size", "description", "faviconUrl"};
QIcon bookIcon;
for (auto bookId : bookIds) {
auto mp = getBookInfos(bookId, keys);
bookList.append(mp);
}
return bookList;
}
void ContentManager::onCustomContextMenu(const QPoint &point)
{
QModelIndex index = mp_view->getView()->indexAt(point);
QMenu contextMenu("optionsMenu", mp_view->getView());
auto bookNode = static_cast<RowNode*>(index.internalPointer());
const auto id = bookNode->getBookId();
QAction menuDeleteBook("Delete book", this);
QAction menuOpenBook("Open book", this);
QAction menuDownloadBook("Download book", this);
QAction menuPauseBook("Pause download", this);
QAction menuResumeBook("Resume download", this);
QAction menuCancelBook("Cancel download", this);
if (bookNode->isDownloading()) {
if (bookNode->getDownloadInfo().paused) {
contextMenu.addAction(&menuResumeBook);
} else {
contextMenu.addAction(&menuPauseBook);
}
contextMenu.addAction(&menuCancelBook);
} else {
if (m_local) {
contextMenu.addAction(&menuOpenBook);
contextMenu.addAction(&menuDeleteBook);
}
else
contextMenu.addAction(&menuDownloadBook);
}
connect(&menuDeleteBook, &QAction::triggered, [=]() {
eraseBook(id);
});
connect(&menuOpenBook, &QAction::triggered, [=]() {
openBook(id);
});
connect(&menuDownloadBook, &QAction::triggered, [=]() {
downloadBook(id, index);
});
connect(&menuPauseBook, &QAction::triggered, [=]() {
pauseBook(id, index);
});
connect(&menuCancelBook, &QAction::triggered, [=]() {
cancelBook(id, index);
});
connect(&menuResumeBook, &QAction::triggered, [=]() {
resumeBook(id, index);
});
if (index.isValid()) {
contextMenu.exec(mp_view->getView()->viewport()->mapToGlobal(point));
}
}
void ContentManager::setLocal(bool local) {
@ -47,15 +155,16 @@ QStringList ContentManager::getTranslations(const QStringList &keys)
return translations;
}
#define ADD_V(KEY, METH) {if(key==KEY) values.append(QString::fromStdString((b->METH())));}
QStringList ContentManager::getBookInfos(QString id, const QStringList &keys)
#define ADD_V(KEY, METH) {if(key==KEY) values.insert(key, QString::fromStdString((b->METH())));}
QMap<QString, QVariant> ContentManager::getBookInfos(QString id, const QStringList &keys)
{
QStringList values;
QMap<QString, QVariant> values;
const kiwix::Book* b = [=]()->const kiwix::Book* {
try {
return &mp_library->getBookById(id);
} catch (...) {
try {
QMutexLocker locker(&remoteLibraryLocker);
return &m_remoteLibrary.getBookById(id.toStdString());
} catch(...) { return nullptr; }
}
@ -64,7 +173,7 @@ QStringList ContentManager::getBookInfos(QString id, const QStringList &keys)
if (nullptr == b){
for(auto& key:keys) {
(void) key;
values.append("");
values.insert(key, "");
}
return values;
}
@ -87,7 +196,7 @@ QStringList ContentManager::getBookInfos(QString id, const QStringList &keys)
const kiwix::Book::Illustration tempIllustration;
mimeType = tempIllustration.mimeType;
}
values.append(QString::fromStdString(mimeType));
values.insert(key, QString::fromStdString(mimeType));
}
if (key == "faviconUrl") {
std::string url;
@ -98,10 +207,10 @@ QStringList ContentManager::getBookInfos(QString id, const QStringList &keys)
const kiwix::Book::Illustration tempIllustration;
url = tempIllustration.url;
}
values.append(QString::fromStdString(url));
values.insert(key, QString::fromStdString(url));
}
if (key == "size") {
values.append(QString::number(b->getSize()));
values.insert(key, QString::number(b->getSize()));
}
if (key == "tags") {
QStringList tagList = QString::fromStdString(b->getTags()).split(';');
@ -117,13 +226,27 @@ QStringList ContentManager::getBookInfos(QString id, const QStringList &keys)
if (displayTagMap["_pictures"]) displayTagList << tr("Pictures");
if (!displayTagMap["_details"]) displayTagList << tr("Introduction only");
QString s = displayTagList.join(", ");
values.append(s);
values.insert(key, s);
}
}
return values;
}
#undef ADD_V
void ContentManager::openBookWithIndex(const QModelIndex &index)
{
try {
QString bookId;
auto bookNode = static_cast<Node*>(index.internalPointer());
bookId = bookNode->getBookId();
// check if the book is available in local library, will throw std::out_of_range if it isn't.
KiwixApp::instance()->getLibrary()->getBookById(bookId);
if (getBookInfos(bookId, {"downloadId"})["downloadId"] != "")
return;
openBook(bookId);
} catch (std::out_of_range &e) {}
}
void ContentManager::openBook(const QString &id)
{
QUrl url("zim://"+id+".zim/");
@ -143,14 +266,14 @@ void ContentManager::openBook(const QString &id)
}
}
#define ADD_V(KEY, METH) {if(key==KEY) {values.append(QString::fromStdString((d->METH()))); continue;}}
QStringList ContentManager::updateDownloadInfos(QString id, const QStringList &keys)
#define ADD_V(KEY, METH) {if(key==KEY) {values.insert(key, QString::fromStdString((d->METH()))); continue;}}
QMap<QString, QVariant> ContentManager::updateDownloadInfos(QString id, const QStringList &keys)
{
QStringList values;
QMap<QString, QVariant> values;
if (!mp_downloader) {
for(auto& key: keys) {
(void) key;
values.append("");
values.insert(key, "");
}
return values;
}
@ -190,53 +313,77 @@ QStringList ContentManager::updateDownloadInfos(QString id, const QStringList &k
if(key == "status") {
switch(d->getStatus()){
case kiwix::Download::K_ACTIVE:
values.append("active");
values.insert(key, "active");
break;
case kiwix::Download::K_WAITING:
values.append("waiting");
values.insert(key, "waiting");
break;
case kiwix::Download::K_PAUSED:
values.append("paused");
values.insert(key, "paused");
break;
case kiwix::Download::K_ERROR:
values.append("error");
values.insert(key, "error");
break;
case kiwix::Download::K_COMPLETE:
values.append("completed");
values.insert(key, "completed");
break;
case kiwix::Download::K_REMOVED:
values.append("removed");
values.insert(key, "removed");
break;
default:
values.append("unknown");
values.insert(key, "unknown");
}
continue;
}
ADD_V("followedBy", getFollowedBy);
ADD_V("path", getPath);
if(key == "totalLength") {
values.append(QString::number(d->getTotalLength()));
values.insert(key, QString::number(d->getTotalLength()));
}
if(key == "completedLength") {
values.append(QString::number(d->getCompletedLength()));
values.insert(key, QString::number(d->getCompletedLength()));
}
if(key == "downloadSpeed") {
values.append(QString::number(d->getDownloadSpeed()));
values.insert(key, QString::number(d->getDownloadSpeed()));
}
if(key == "verifiedLength") {
values.append(QString::number(d->getVerifiedLength()));
values.insert(key, QString::number(d->getVerifiedLength()));
}
}
return values;
}
#undef ADD_V
QString ContentManager::downloadBook(const QString &id, QModelIndex index)
{
QString downloadStatus = downloadBook(id);
QString dialogHeader, dialogText;
if (downloadStatus.size() == 0) {
dialogHeader = gt("download-unavailable");
dialogText = gt("download-unavailable-text");
} else if (downloadStatus == "storage_error") {
dialogHeader = gt("download-storage-error");
dialogText = gt("download-storage-error-text");
} else {
emit managerModel->startDownload(index);
return downloadStatus;
}
KiwixConfirmBox *dialog = new KiwixConfirmBox(dialogHeader, dialogText, true, mp_view);
dialog->show();
connect(dialog, &KiwixConfirmBox::okClicked, [=]() {
dialog->deleteLater();
});
return downloadStatus;
}
QString ContentManager::downloadBook(const QString &id)
{
if (!mp_downloader)
return "";
const auto& book = [&]()->const kiwix::Book& {
try {
QMutexLocker locker(&remoteLibraryLocker);
return m_remoteLibrary.getBookById(id.toStdString());
} catch (...) {
return mp_library->getBookById(id);
@ -281,21 +428,38 @@ void ContentManager::eraseBookFilesFromComputer(const QString dirPath, const QSt
void ContentManager::eraseBook(const QString& id)
{
auto tabBar = KiwixApp::instance()->getTabWidget();
tabBar->closeTabsByZimId(id);
kiwix::Book book = mp_library->getBookById(id);
QString dirPath = QString::fromStdString(kiwix::removeLastPathElement(book.getPath()));
QString fileName = QString::fromStdString(kiwix::getLastPathElement(book.getPath())) + "*";
eraseBookFilesFromComputer(dirPath, fileName);
mp_library->removeBookFromLibraryById(id);
mp_library->save();
emit mp_library->bookmarksChanged();
if (m_local) {
emit(bookRemoved(id));
} else {
emit(oneBookChanged(id));
}
KiwixApp::instance()->getSettingsManager()->deleteSettings(id);
auto text = gt("delete-book-text");
text = text.replace("{{ZIM}}", QString::fromStdString(mp_library->getBookById(id).getTitle()));
KiwixConfirmBox *dialog = new KiwixConfirmBox(gt("delete-book"), text, false, mp_view);
dialog->show();
connect(dialog, &KiwixConfirmBox::yesClicked, [=]() {
auto tabBar = KiwixApp::instance()->getTabWidget();
tabBar->closeTabsByZimId(id);
kiwix::Book book = mp_library->getBookById(id);
QString dirPath = QString::fromStdString(kiwix::removeLastPathElement(book.getPath()));
QString fileName = QString::fromStdString(kiwix::getLastPathElement(book.getPath())) + "*";
eraseBookFilesFromComputer(dirPath, fileName);
mp_library->removeBookFromLibraryById(id);
mp_library->save();
emit mp_library->bookmarksChanged();
if (m_local) {
emit(bookRemoved(id));
} else {
emit(oneBookChanged(id));
}
KiwixApp::instance()->getSettingsManager()->deleteSettings(id);
dialog->deleteLater();
emit booksChanged();
});
connect(dialog, &KiwixConfirmBox::noClicked, [=]() {
dialog->deleteLater();
});
}
void ContentManager::pauseBook(const QString& id, QModelIndex index)
{
pauseBook(id);
emit managerModel->pauseDownload(index);
}
void ContentManager::pauseBook(const QString& id)
@ -309,6 +473,12 @@ void ContentManager::pauseBook(const QString& id)
download->pauseDownload();
}
void ContentManager::resumeBook(const QString& id, QModelIndex index)
{
resumeBook(id);
emit managerModel->resumeDownload(index);
}
void ContentManager::resumeBook(const QString& id)
{
if (!mp_downloader) {
@ -320,6 +490,22 @@ void ContentManager::resumeBook(const QString& id)
download->resumeDownload();
}
void ContentManager::cancelBook(const QString& id, QModelIndex index)
{
auto text = gt("cancel-download-text");
text = text.replace("{{ZIM}}", QString::fromStdString(mp_library->getBookById(id).getTitle()));
KiwixConfirmBox *dialog = new KiwixConfirmBox(gt("cancel-download"), text, false, mp_view);
dialog->show();
connect(dialog, &KiwixConfirmBox::yesClicked, [=]() {
cancelBook(id);
emit managerModel->cancelDownload(index);
dialog->deleteLater();
});
connect(dialog, &KiwixConfirmBox::noClicked, [=]() {
dialog->deleteLater();
});
}
void ContentManager::cancelBook(const QString& id)
{
if (!mp_downloader) {
@ -389,11 +575,14 @@ void ContentManager::updateLibrary() {
#define CATALOG_URL "library.kiwix.org"
void ContentManager::updateRemoteLibrary(const QString& content) {
m_remoteLibrary = kiwix::Library();
kiwix::Manager manager(&m_remoteLibrary);
manager.readOpds(content.toStdString(), CATALOG_URL);
emit(this->booksChanged());
emit(this->pendingRequest(false));
QtConcurrent::run([=]() {
QMutexLocker locker(&remoteLibraryLocker);
m_remoteLibrary = kiwix::Library();
kiwix::Manager manager(&m_remoteLibrary);
manager.readOpds(content.toStdString(), CATALOG_URL);
emit(this->booksChanged());
emit(this->pendingRequest(false));
});
}
void ContentManager::setSearch(const QString &search)
@ -439,6 +628,7 @@ QStringList ContentManager::getBookIds()
return mp_library->listBookIds(filter, m_sortBy, m_sortOrderAsc);
} else {
filter.remote(true);
QMutexLocker locker(&remoteLibraryLocker);
auto bookIds = m_remoteLibrary.filter(filter);
m_remoteLibrary.sort(bookIds, m_sortBy, m_sortOrderAsc);
QStringList list;

View File

@ -8,6 +8,7 @@
#include <kiwix/downloader.h>
#include "opdsrequestmanager.h"
#include "contenttypefilter.h"
#include "contentmanagermodel.h"
class ContentManager : public QObject
{
@ -26,6 +27,7 @@ public:
void setCurrentLanguage(QString language);
void setCurrentCategoryFilter(QString category);
void setCurrentContentTypeFilter(QList<ContentTypeFilter*>& contentTypeFilter);
bool isLocal() const { return m_local; }
private:
Library* mp_library;
@ -43,6 +45,9 @@ private:
QStringList getBookIds();
void eraseBookFilesFromComputer(const QString dirPath, const QString filename);
QList<QMap<QString, QVariant>> getBooksList();
ContentManagerModel *managerModel;
QMutex remoteLibraryLocker;
signals:
void filterParamsChanged();
@ -55,10 +60,11 @@ signals:
public slots:
QStringList getTranslations(const QStringList &keys);
QStringList getBookInfos(QString id, const QStringList &keys);
QMap<QString, QVariant> getBookInfos(QString id, const QStringList &keys);
void openBook(const QString& id);
QStringList updateDownloadInfos(QString id, const QStringList& keys);
QMap<QString, QVariant> updateDownloadInfos(QString id, const QStringList& keys);
QString downloadBook(const QString& id);
QString downloadBook(const QString& id, QModelIndex index);
void updateLibrary();
void setSearch(const QString& search);
void setSortBy(const QString& sortBy, const bool sortOrderAsc);
@ -67,6 +73,11 @@ public slots:
void pauseBook(const QString& id);
void resumeBook(const QString& id);
void cancelBook(const QString& id);
void pauseBook(const QString& id, QModelIndex index);
void resumeBook(const QString& id, QModelIndex index);
void cancelBook(const QString& id, QModelIndex index);
void onCustomContextMenu(const QPoint &point);
void openBookWithIndex(const QModelIndex& index);
};
#endif // CONTENTMANAGER_H

View File

@ -0,0 +1,273 @@
#include <QtGui>
#include "contentmanagerdelegate.h"
#include <QApplication>
#include <QDialog>
#include <QStyleOptionViewItemV4>
#include "kiwixapp.h"
#include <QStyleOptionViewItem>
#include "rownode.h"
#include "descriptionnode.h"
ContentManagerDelegate::ContentManagerDelegate(QObject *parent)
: QStyledItemDelegate(parent), baseButton(new QPushButton)
{
baseButton->setStyleSheet("background-color: white;"
"border: 0;"
"font-weight: bold;"
"font-family: Selawik;"
"color: blue;"
"margin: 4px;");
QImage placeholderIconFile(":/icons/placeholder-icon.png");
QBuffer buffer(&placeholderIcon);
buffer.open(QIODevice::WriteOnly);
placeholderIconFile.save(&buffer, "png");
}
void createPauseSymbol(QPainter *painter, int x, int y)
{
QPen pen;
pen.setWidth(3);
QPainterPath path;
x += 12.5;
y += 10;
pen.setColor("#3366cc");
path.moveTo(x, y);
path.lineTo(x, y + 10);
painter->strokePath(path, pen);
path.moveTo(x + 5, y);
path.lineTo(x + 5, y + 10);
painter->strokePath(path, pen);
}
void createResumeSymbol(QPainter *painter, int x, int y)
{
QPen pen;
pen.setWidth(3);
QPainterPath path;
x += 12.5;
y += 8;
pen.setColor("#3366cc");
path.moveTo(x, y);
path.lineTo(x, y + 15);
path.lineTo(x + 10, y + 8);
path.lineTo(x, y);
painter->setRenderHint(QPainter::Antialiasing);
painter->strokePath(path, pen);
}
void createArc(QPainter *painter, int startAngle, int spanAngle, QRect rectangle, QPen pen)
{
painter->setRenderHint(QPainter::Antialiasing);
int arcX = rectangle.x();
int arcY = rectangle.y();
int arcW = rectangle.width();
int arcH = rectangle.height();
QPainterPath path;
path.moveTo(arcX + arcW, arcY + arcH/2);
path.arcTo(rectangle, startAngle, spanAngle);
painter->strokePath(path, pen);
}
void createCancelSymbol(QPainter *painter, int x, int y, int w, int h)
{
QPen p;
p.setWidth(3);
p.setColor("#dd3333");
QRect r(x, y, w, h);
createArc(painter, 0, 360, r, p);
painter->setPen(p);
QRect nRect(x, y, w, h);
auto oldFont = painter->font();
auto bFont = oldFont;
bFont.setBold(true);
painter->setFont(bFont);
painter->drawText(nRect, Qt::AlignCenter | Qt::AlignJustify, "X");
painter->setFont(oldFont);
}
void createDownloadStats(QPainter *painter, QRect box, QString downloadSpeed, QString completedLength)
{
QPen pen;
int x = box.x();
int y = box.y();
int w = box.width();
int h = box.height();
pen.setColor("#666666");
painter->setPen(pen);
auto oldFont = painter->font();
painter->setFont(QFont("Selawik", 8));
QRect nRect(x - 10, y - 10, w, h);
painter->drawText(nRect,Qt::AlignCenter | Qt::AlignJustify, downloadSpeed);
QRect fRect(x - 10, y + 10, w, h);
painter->drawText(fRect,Qt::AlignCenter | Qt::AlignJustify, completedLength);
painter->setFont(oldFont);
}
void showDownloadProgress(QPainter *painter, QRect box, DownloadInfo downloadInfo)
{
int x,y,w,h;
x = box.left();
y = box.top();
w = box.width();
h = box.height();
int arcX = x + w/2 + 20;
int arcY = y + 20;
int arcW = w - 90;
int arcH = h - 40;
double progress = (double) (downloadInfo.progress) / 100;
progress = -progress;
auto completedLength = downloadInfo.completedLength;
auto downloadSpeed = downloadInfo.downloadSpeed;
if (downloadInfo.paused) {
createResumeSymbol(painter, arcX, arcY);
createCancelSymbol(painter, x + w/2 - 20, arcY, arcW, arcH);
} else {
createPauseSymbol(painter, arcX, arcY);
createDownloadStats(painter, box, downloadSpeed, completedLength);
}
QPen pen;
pen.setWidth(3);
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);
QRect rectangle(arcX, arcY, arcW, arcH);
pen.setColor("#eaecf0");
createArc(painter, 0, 360, rectangle, pen);
int startAngle = 0;
int spanAngle = progress * 360;
pen.setColor("#3366cc");
createArc(painter, startAngle, spanAngle, rectangle, pen);
}
void ContentManagerDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionButton button;
QRect r = option.rect;
int x,y,w,h;
x = r.left();
y = r.top();
w = r.width();
h = r.height();
button.rect = QRect(x,y,w,h);
button.state = QStyle::State_Enabled;
if (index.parent().isValid()) {
// additional info
QRect nRect = r;
auto viewWidth = KiwixApp::instance()->getContentManager()->getView()->getView()->width();
nRect.setWidth(viewWidth);
painter->drawText(nRect, Qt::AlignLeft | Qt::AlignVCenter, index.data(Qt::UserRole+1).toString());
return;
}
auto node = static_cast<RowNode*>(index.internalPointer());
try {
const auto id = node->getBookId();
const auto book = KiwixApp::instance()->getLibrary()->getBookById(id);
if(KiwixApp::instance()->getContentManager()->getBookInfos(id, {"downloadId"})["downloadId"] != "") {
} else {
button.text = gt("open");
}
} catch (std::out_of_range& e) {
button.text = gt("download");
}
QStyleOptionViewItem eOpt = option;
if (index.column() == 5) {
if (node->isDownloading()) {
auto downloadInfo = node->getDownloadInfo();
showDownloadProgress(painter, r, downloadInfo);
}
else {
baseButton->style()->drawControl( QStyle::CE_PushButton, &button, painter, baseButton.data());
}
return;
}
if (index.column() == 0) {
auto iconData = index.data().value<QByteArray>();
if (iconData.isNull())
iconData = placeholderIcon;
QPixmap pix;
pix.loadFromData(iconData);
QIcon icon(pix);
icon.paint(painter, QRect(x+10, y+10, 30, 50));
return;
}
if (index.column() == 1) {
auto bFont = painter->font();
bFont.setWeight(60);
eOpt.font = bFont;
}
QStyledItemDelegate::paint(painter, eOpt, index);
}
bool ContentManagerDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
if(event->type() == QEvent::MouseButtonRelease )
{
QMouseEvent * e = (QMouseEvent *)event;
int clickX = e->x();
int clickY = e->y();
QRect r = option.rect;
int x,y,w,h;
x = r.left();
y = r.top();
w = r.width();
h = r.height();
if (e->button() == Qt::MiddleButton && index.column() != 5) {
KiwixApp::instance()->getContentManager()->openBookWithIndex(index);
return true;
}
const auto lastColumnClicked = ((index.column() == 5) && (clickX > x && clickX < x + w)
&& (clickY > y && clickY < y + h));
if (lastColumnClicked)
handleLastColumnClicked(index, e, option);
}
return true;
}
void ContentManagerDelegate::handleLastColumnClicked(const QModelIndex& index, QMouseEvent *mouseEvent, const QStyleOptionViewItem &option)
{
const auto node = static_cast<RowNode*>(index.internalPointer());
const auto id = node->getBookId();
int clickX = mouseEvent->x();
QRect r = option.rect;
int x = r.left();
int w = r.width();
if (node->isDownloading()) {
if (node->getDownloadInfo().paused) {
if (clickX < (x + w/2)) {
KiwixApp::instance()->getContentManager()->cancelBook(id, index);
} else {
KiwixApp::instance()->getContentManager()->resumeBook(id, index);
}
} else {
KiwixApp::instance()->getContentManager()->pauseBook(id, index);
}
} else {
try {
const auto book = KiwixApp::instance()->getLibrary()->getBookById(id);
KiwixApp::instance()->getContentManager()->openBook(id);
} catch (std::out_of_range& e) {
KiwixApp::instance()->getContentManager()->downloadBook(id, index);
}
}
}
QSize ContentManagerDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.parent().isValid()) {
return QSize(300, 70);
}
return QSize(50, 70);
}

View File

@ -0,0 +1,24 @@
#ifndef CONTENTMANAGERDELEGATE_H
#define CONTENTMANAGERDELEGATE_H
#include <QStyledItemDelegate>
#include <QPushButton>
#include <QByteArray>
class ContentManagerDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
ContentManagerDelegate(QObject *parent=0);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
QScopedPointer<QPushButton> baseButton;
QByteArray placeholderIcon;
void handleLastColumnClicked(const QModelIndex& index, QMouseEvent *event, const QStyleOptionViewItem &option);
};
#endif // CONTENTMANAGERDELEGATE_H

View File

@ -0,0 +1,20 @@
#include "contentmanagerheader.h"
#include <QPainter>
ContentManagerHeader::ContentManagerHeader(Qt::Orientation orientation, QWidget *parent)
: QHeaderView(orientation, parent)
{}
ContentManagerHeader::~ContentManagerHeader()
{}
void ContentManagerHeader::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const
{
// This is required so that the sort indicator icon is not shown in first column
if (logicalIndex == 0)
return;
painter->save();
QHeaderView::paintSection(painter, rect, logicalIndex);
painter->restore();
}

View File

@ -0,0 +1,18 @@
#ifndef CONTENTMANAGERHEADER_H
#define CONTENTMANAGERHEADER_H
#include <QHeaderView>
class ContentManagerHeader : public QHeaderView
{
Q_OBJECT
public:
explicit ContentManagerHeader(Qt::Orientation orientation, QWidget *parent = nullptr);
~ContentManagerHeader();
protected:
void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const override;
};
#endif // CONTENTMANAGERHEADER_H

279
src/contentmanagermodel.cpp Normal file
View File

@ -0,0 +1,279 @@
#include "contentmanagermodel.h"
#include "node.h"
#include "rownode.h"
#include "descriptionnode.h"
#include <zim/error.h>
#include <zim/item.h>
#include "kiwixapp.h"
ContentManagerModel::ContentManagerModel(QObject *parent)
: QAbstractItemModel(parent)
{
connect(&td, &ThumbnailDownloader::oneThumbnailDownloaded, this, &ContentManagerModel::updateImage);
}
ContentManagerModel::~ContentManagerModel()
{
}
int ContentManagerModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return static_cast<Node*>(parent.internalPointer())->columnCount();
return rootNode->columnCount();
}
QVariant ContentManagerModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return QVariant();
auto item = static_cast<Node*>(index.internalPointer());
const auto displayRole = role == Qt::DisplayRole;
const auto additionalInfoRole = role == Qt::UserRole+1;
if (displayRole || additionalInfoRole)
return item->data(index.column());
return QVariant();
}
Qt::ItemFlags ContentManagerModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
if (index.isValid() && index.parent().isValid()) {
return defaultFlags & ~Qt::ItemIsDropEnabled & ~Qt::ItemIsDragEnabled & ~Qt::ItemIsSelectable & ~Qt::ItemIsEditable & ~Qt::ItemIsUserCheckable;
}
return defaultFlags;
}
QModelIndex ContentManagerModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
RowNode* parentItem;
if (!parent.isValid()) {
parentItem = rootNode.get();
}
else {
parentItem = static_cast<RowNode*>(parent.internalPointer());
}
auto childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem.get());
return QModelIndex();
}
QModelIndex ContentManagerModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
auto childItem = static_cast<Node*>(index.internalPointer());
auto parentItem = childItem->parentItem();
if (!parentItem || parentItem == rootNode)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem.get());
}
int ContentManagerModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return zimCount;
}
QVariant ContentManagerModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole || orientation != Qt::Horizontal)
return QVariant();
switch (section)
{
case 1: return "Name";
case 2: return "Date";
case 3: return "Size";
case 4: return "Content Type";
default: return QVariant();
}
}
void ContentManagerModel::setBooksData(const QList<QMap<QString, QVariant>>& data)
{
m_data = data;
rootNode = std::shared_ptr<RowNode>(new RowNode({tr("Icon"), tr("Name"), tr("Date"), tr("Size"), tr("Content Type"), tr("Download")}, "", std::weak_ptr<RowNode>()));
setupNodes();
emit dataChanged(QModelIndex(), QModelIndex());
}
QString convertToUnits(QString size)
{
QStringList units = {"bytes", "KB", "MB", "GB", "TB", "PB", "EB"};
int unitIndex = 0;
auto bytes = size.toDouble();
while (bytes >= 1024 && unitIndex < units.size()) {
bytes /= 1024;
unitIndex++;
}
const auto preciseBytes = QString::number(bytes, 'g', 3);
return preciseBytes + " " + units[unitIndex];
}
void ContentManagerModel::setupNodes()
{
beginResetModel();
for (auto bookItem : m_data) {
rootNode->appendChild(RowNode::createNode(bookItem, iconMap, rootNode));
}
endResetModel();
}
void ContentManagerModel::refreshIcons()
{
if (KiwixApp::instance()->getContentManager()->isLocal())
return;
td.clearQueue();
for (auto i = 0; i < rowCount() && i < m_data.size(); i++) {
auto bookItem = m_data[i];
auto id = bookItem["id"].toString();
auto faviconUrl = "https://" + bookItem["faviconUrl"].toString();
auto app = KiwixApp::instance();
try {
auto book = app->getLibrary()->getBookById(id);
auto item = book.getIllustration(48);
} catch (...) {
if (faviconUrl != "" && !iconMap.contains(faviconUrl)) {
td.addDownload(faviconUrl, index(i, 0));
}
}
}
}
bool ContentManagerModel::hasChildren(const QModelIndex &parent) const
{
auto item = static_cast<Node*>(parent.internalPointer());
if (item)
return item->childCount() > 0;
return true;
}
bool ContentManagerModel::canFetchMore(const QModelIndex &parent) const
{
if (parent.isValid())
return false;
return (zimCount < m_data.size());
}
void ContentManagerModel::fetchMore(const QModelIndex &parent)
{
if (parent.isValid())
return;
int remainder = m_data.size() - zimCount;
int zimsToFetch = qMin(5, remainder);
beginInsertRows(QModelIndex(), zimCount, zimCount + zimsToFetch - 1);
zimCount += zimsToFetch;
endInsertRows();
refreshIcons();
}
void ContentManagerModel::sort(int column, Qt::SortOrder order)
{
if (column == 0 || column == 4 || column == 5)
return;
QString sortBy = "";
switch(column) {
case 1:
sortBy = "title";
break;
case 2:
sortBy = "date";
break;
case 3:
sortBy = "size";
break;
default:
sortBy = "unsorted";
}
KiwixApp::instance()->getContentManager()->setSortBy(sortBy, order == Qt::AscendingOrder);
}
void ContentManagerModel::updateImage(QModelIndex index, QString url, QByteArray imageData)
{
if (!index.isValid())
return;
auto item = static_cast<RowNode*>(index.internalPointer());
if (!rootNode->isChild(item))
return;
item->setIconData(imageData);
iconMap[url] = imageData;
emit dataChanged(index, index);
}
std::shared_ptr<RowNode> getSharedPointer(RowNode* ptr)
{
return std::static_pointer_cast<RowNode>(ptr->shared_from_this());
}
void ContentManagerModel::startDownload(QModelIndex index)
{
auto node = getSharedPointer(static_cast<RowNode*>(index.internalPointer()));
node->setIsDownloading(true);
auto id = node->getBookId();
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [=]() {
auto downloadInfos = KiwixApp::instance()->getContentManager()->updateDownloadInfos(id, {"status", "completedLength", "totalLength", "downloadSpeed"});
double percent = (double) downloadInfos["completedLength"].toInt() / downloadInfos["totalLength"].toInt();
percent *= 100;
percent = QString::number(percent, 'g', 3).toDouble();
auto completedLength = convertToUnits(downloadInfos["completedLength"].toString());
auto downloadSpeed = convertToUnits(downloadInfos["downloadSpeed"].toString()) + "/s";
node->setDownloadInfo({percent, completedLength, downloadSpeed});
if (!downloadInfos["status"].isValid()) {
node->setIsDownloading(false);
timer->stop();
timer->deleteLater();
}
emit dataChanged(index, index);
});
timer->start(1000);
timers[id] = timer;
}
void ContentManagerModel::pauseDownload(QModelIndex index)
{
auto node = static_cast<RowNode*>(index.internalPointer());
auto id = node->getBookId();
auto prevDownloadInfo = node->getDownloadInfo();
prevDownloadInfo.paused = true;
node->setDownloadInfo(prevDownloadInfo);
timers[id]->stop();
emit dataChanged(index, index);
}
void ContentManagerModel::resumeDownload(QModelIndex index)
{
auto node = static_cast<RowNode*>(index.internalPointer());
auto id = node->getBookId();
auto prevDownloadInfo = node->getDownloadInfo();
prevDownloadInfo.paused = false;
node->setDownloadInfo(prevDownloadInfo);
timers[id]->start(1000);
emit dataChanged(index, index);
}
void ContentManagerModel::cancelDownload(QModelIndex index)
{
auto node = static_cast<RowNode*>(index.internalPointer());
auto id = node->getBookId();
node->setIsDownloading(false);
node->setDownloadInfo({0, "", "", false});
timers[id]->stop();
timers[id]->deleteLater();
emit dataChanged(index, index);
}

58
src/contentmanagermodel.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef CONTENTMANAGERMODEL_H
#define CONTENTMANAGERMODEL_H
#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include <QIcon>
#include "thumbnaildownloader.h"
#include <memory>
class RowNode;
class Node;
class DescriptionNode;
class ContentManagerModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit ContentManagerModel(QObject *parent = nullptr);
~ContentManagerModel();
QVariant data(const QModelIndex &index, int role) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
void setBooksData(const QList<QMap<QString, QVariant>>& data);
void setupNodes();
bool hasChildren(const QModelIndex &parent) const override;
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void refreshIcons();
public slots:
void updateImage(QModelIndex index, QString url, QByteArray imageData);
void startDownload(QModelIndex index);
void pauseDownload(QModelIndex index);
void resumeDownload(QModelIndex index);
void cancelDownload(QModelIndex index);
protected:
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
private:
QList<QMap<QString, QVariant>> m_data;
std::shared_ptr<RowNode> rootNode;
int zimCount = 0;
ThumbnailDownloader td;
QMap<QString, QByteArray> iconMap;
QMap<QString, QTimer*> timers;
};
#endif // CONTENTMANAGERMODEL_H

View File

@ -54,6 +54,18 @@ ContentManagerSide::ContentManagerSide(QWidget *parent) :
}
mp_contentManager->setCurrentContentTypeFilter(m_contentTypeFilters);
});
auto searcher = mp_ui->searcher;
searcher->setPlaceholderText(gt("search-files"));
QFile file(QString::fromUtf8(":/css/_contentManager.css"));
file.open(QFile::ReadOnly);
QString styleSheet = QString(file.readAll());
searcher->setStyleSheet(styleSheet);
QIcon searchIcon = QIcon(":/icons/search.svg");
searcher->addAction(searchIcon, QLineEdit::LeadingPosition);
connect(searcher, &QLineEdit::textChanged, [searcher](){
KiwixApp::instance()->getContentManager()->setSearch(searcher->text());
});
ContentTypeFilter* videosFilter = new ContentTypeFilter("pictures", this);
ContentTypeFilter* picturesFilter = new ContentTypeFilter("videos", this);

View File

@ -46,7 +46,7 @@
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,0,0,0,0,0,0">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,0,0,0,0,0,0,0">
<property name="spacing">
<number>0</number>
</property>
@ -65,6 +65,9 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="searcher"/>
</item>
<item>
<widget class="QRadioButton" name="allFileButton">
<property name="sizePolicy">

View File

@ -1,29 +1,51 @@
#include "contentmanagerview.h"
#include <QFile>
#include <QWebEngineProfile>
#include "kiwixapp.h"
#include "contentmanagerdelegate.h"
#include <QLineEdit>
#include "ui_contentmanagerview.h"
ContentManagerView::ContentManagerView(QWidget *parent)
: QWebEngineView(parent)
: QWidget(parent), mp_ui(new Ui::contentmanagerview)
{
QWebEnginePage* page = new QWebEnginePage(KiwixApp::instance()->getProfile(), this);
setPage(page);
page->setWebChannel(&m_webChannel);
setContextMenuPolicy( Qt::NoContextMenu );
mp_ui->setupUi(this);
mp_ui->m_view->setSortingEnabled(true);
QFile file(QString::fromUtf8(":/css/_contentManager.css"));
file.open(QFile::ReadOnly);
QString styleSheet = QString(file.readAll());
mp_ui->m_view->setStyleSheet(styleSheet);
mp_ui->m_view->setContextMenuPolicy(Qt::CustomContextMenu);
auto managerDelegate = new ContentManagerDelegate();
mp_ui->m_view->setItemDelegate(managerDelegate);
mp_ui->m_view->setCursor(Qt::PointingHandCursor);
loader = new KiwixLoader(mp_ui->loading);
mp_ui->stackedWidget->setCurrentIndex(0);
connect(mp_ui->m_view, &QTreeView::clicked, [=](QModelIndex index) {
if (index.column() == (mp_ui->m_view->model()->columnCount() - 1))
return;
auto zeroColIndex = index.siblingAtColumn(0);
if (mp_ui->m_view->isExpanded(zeroColIndex)) {
mp_ui->m_view->collapse(zeroColIndex);
} else {
mp_ui->m_view->expand(zeroColIndex);
}
});
}
void ContentManagerView::registerObject(const QString& id, QObject* object)
ContentManagerView::~ContentManagerView()
{
m_webChannel.registerObject(id, object);
}
void ContentManagerView::setHtml()
void ContentManagerView::showLoader(bool show)
{
QFile contentFile(":texts/_contentManager.html");
contentFile.open(QIODevice::ReadOnly);
auto byteContent = contentFile.readAll();
contentFile.close();
QWebEngineView::setHtml(byteContent);
mp_ui->stackedWidget->setCurrentIndex(show);
if (show) {
loader->startAnimation();
} else {
loader->stopAnimation();
}
}

View File

@ -1,18 +1,29 @@
#ifndef CONTENTMANAGERVIEW_H
#define CONTENTMANAGERVIEW_H
#include <QWebEngineView>
#include <QWebChannel>
#include <QWidget>
#include "ui_contentmanagerview.h"
#include "kiwixloader.h"
class ContentManagerView : public QWebEngineView
namespace Ui {
class contentmanagerview;
}
class ContentManagerView : public QWidget
{
Q_OBJECT
public:
ContentManagerView(QWidget *parent = Q_NULLPTR);
void registerObject(const QString &id, QObject *object);
void setHtml();
explicit ContentManagerView(QWidget *parent = nullptr);
~ContentManagerView();
QTreeView* getView() { return mp_ui->m_view; }
public slots:
void showLoader(bool show);
private:
QWebChannel m_webChannel;
Ui::contentmanagerview *mp_ui;
KiwixLoader *loader;
};
#endif // CONTENTMANAGERVIEW_H

49
src/contentmanagerview.ui Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>contentmanagerview</class>
<widget class="QWidget" name="contentmanagerview">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="contents">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QTreeView" name="m_view"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="loading">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QWidget" name="widget" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

50
src/descriptionnode.cpp Normal file
View File

@ -0,0 +1,50 @@
#include "descriptionnode.h"
#include "rownode.h"
DescriptionNode::DescriptionNode(QString desc, std::weak_ptr<RowNode> parent)
: m_desc(desc), m_parentItem(parent)
{}
DescriptionNode::~DescriptionNode()
{}
std::shared_ptr<Node> DescriptionNode::parentItem()
{
std::shared_ptr<Node> temp = m_parentItem.lock();
if (!temp)
return nullptr;
return temp;
}
QString DescriptionNode::getBookId() const
{
std::shared_ptr<RowNode> temp = m_parentItem.lock();
if (!temp)
return QString();
return temp->getBookId();
}
int DescriptionNode::childCount() const
{
return 0;
}
int DescriptionNode::columnCount() const
{
return 1;
}
QVariant DescriptionNode::data(int column)
{
if (column == 1)
return m_desc;
return QVariant();
}
int DescriptionNode::row() const
{
std::shared_ptr<RowNode> temp = m_parentItem.lock();
if (!temp)
return 0;
return temp->row();
}

26
src/descriptionnode.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef DESCRIPTIONNODE_H
#define DESCRIPTIONNODE_H
#include<QString>
#include "node.h"
class RowNode;
class DescriptionNode : public Node
{
public:
DescriptionNode(QString desc, std::weak_ptr<RowNode> parent);
~DescriptionNode();
std::shared_ptr<Node> parentItem() override;
int childCount() const override;
int columnCount() const override;
QVariant data(int column) override;
int row() const override;
QString getBookId() const override;
private:
QString m_desc;
std::weak_ptr<RowNode> m_parentItem;
};
#endif // DESCRIPTIONNODE_H

44
src/kiwixconfirmbox.cpp Normal file
View File

@ -0,0 +1,44 @@
#include "kiwixconfirmbox.h"
#include "ui_kiwixconfirmbox.h"
#include <QFile>
#include "kiwixapp.h"
KiwixConfirmBox::KiwixConfirmBox(QString confirmTitle, QString confirmText, bool okDialog, QWidget *parent) :
QDialog(parent), m_confirmTitle(confirmTitle), m_confirmText(confirmText),
ui(new Ui::kiwixconfirmbox)
{
ui->setupUi(this);
setWindowFlag(Qt::FramelessWindowHint, true);
QFile styleFile(":/css/confirmBox.css");
styleFile.open(QIODevice::ReadOnly);
auto byteContent = styleFile.readAll();
styleFile.close();
QString style(byteContent);
setStyleSheet(style);
connect(ui->yesButton, &QPushButton::clicked, [=]() {
emit yesClicked();
});
connect(ui->noButton, &QPushButton::clicked, [=]() {
emit noClicked();
});
connect(ui->okButton, &QPushButton::clicked, [=]() {
emit okClicked();
});
ui->confirmText->setText(confirmText);
ui->confirmTitle->setText(confirmTitle);
ui->yesButton->setText(gt("yes"));
ui->noButton->setText(gt("no"));
ui->okButton->setText(gt("ok"));
ui->okButton->hide();
if (okDialog) {
ui->yesButton->hide();
ui->noButton->hide();
ui->okButton->show();
}
}
KiwixConfirmBox::~KiwixConfirmBox()
{
delete ui;
}

29
src/kiwixconfirmbox.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef KIWIXCONFIRMBOX_H
#define KIWIXCONFIRMBOX_H
#include <QDialog>
namespace Ui {
class kiwixconfirmbox;
}
class KiwixConfirmBox : public QDialog
{
Q_OBJECT
public:
explicit KiwixConfirmBox(QString confirmTitle, QString confirmText, bool okDialog, QWidget *parent = nullptr);
~KiwixConfirmBox();
signals:
void yesClicked();
void noClicked();
void okClicked();
private:
QString m_confirmTitle;
QString m_confirmText;
Ui::kiwixconfirmbox *ui;
};
#endif // KIWIXCONFIRMBOX_H

75
src/kiwixloader.cpp Normal file
View File

@ -0,0 +1,75 @@
#include "kiwixloader.h"
#include <QPainter>
#include <QPainterPath>
#include <QDebug>
#include <QSizePolicy>
KiwixLoader::KiwixLoader(QWidget *parent)
: QWidget(parent), m_timer(nullptr)
{
setFixedSize(parent->width(), parent->height());
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &KiwixLoader::updateAnimation);
}
KiwixLoader::~KiwixLoader()
{
}
void KiwixLoader::stopAnimation()
{
m_timer->stop();
}
void KiwixLoader::startAnimation()
{
m_timer->start(20);
}
void createArc(QPainter &painter, int startAngle, int spanAngle, QRect rectangle, QPen pen)
{
painter.setRenderHint(QPainter::Antialiasing);
int arcX = rectangle.x();
int arcY = rectangle.y();
int arcW = rectangle.width();
int arcH = rectangle.height();
QPainterPath path;
path.moveTo(arcX + arcW, arcY + arcH/2);
path.arcTo(rectangle, startAngle, spanAngle);
painter.strokePath(path, pen);
}
void KiwixLoader::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
int width = 100;
int height = 100;
setFixedSize(this->parentWidget()->width(), this->parentWidget()->height());
int centerX = this->parentWidget()->width()/2 - width;
int centerY = this->parentWidget()->height()/2 - height;
QPen pen;
pen.setWidth(5);
painter.setPen(pen);
painter.setRenderHint(QPainter::Antialiasing);
QRect rectangle(centerX, centerY, width, height);
pen.setColor("#eaecf0");
createArc(painter, 0, 360, rectangle, pen);
int startAngle = 0;
int spanAngle = -progress;
pen.setColor("#3366cc");
createArc(painter, startAngle, spanAngle, rectangle, pen);
}
void KiwixLoader::updateAnimation()
{
progress += 10;
if (progress == 360)
progress = 0;
update();
}

28
src/kiwixloader.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef KIWIXLOADER_H
#define KIWIXLOADER_H
#include <QWidget>
#include <QTimer>
class KiwixLoader : public QWidget
{
Q_OBJECT
public:
explicit KiwixLoader(QWidget *parent = nullptr);
~KiwixLoader();
void startAnimation();
void stopAnimation();
protected:
void paintEvent(QPaintEvent *event) override;
private slots:
void updateAnimation();
private:
QTimer *m_timer;
int progress = 0;
};
#endif // KIWIXLOADER_H

View File

@ -24,6 +24,7 @@ public:
TabBar* getTabBar();
TopWidget* getTopWidget();
QWidget getMainView();
protected:
void keyPressEvent(QKeyEvent *event);

20
src/node.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef NODE_H
#define NODE_H
#include <QVariant>
#include <memory>
class Node : public std::enable_shared_from_this<Node>
{
public:
virtual ~Node() = default;
virtual std::shared_ptr<Node> parentItem() = 0;
virtual int childCount() const = 0;
virtual int columnCount() const = 0;
virtual QVariant data(int column) = 0;
virtual int row() const = 0;
virtual QString getBookId() const = 0;
};
#endif // NODE_H

107
src/rownode.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "rownode.h"
#include <QVariant>
#include "kiwixapp.h"
#include "descriptionnode.h"
#include "kiwix/tools.h"
RowNode::RowNode(QList<QVariant> itemData, QString bookId, std::weak_ptr<RowNode> parent)
: m_itemData(itemData), m_parentItem(parent), m_bookId(bookId)
{
m_downloadInfo = {0, "", "", false};
}
RowNode::~RowNode()
{}
void RowNode::appendChild(std::shared_ptr<Node> item)
{
m_childItems.append(item);
}
std::shared_ptr<Node> RowNode::child(int row)
{
if (row < 0 || row >= m_childItems.size())
return nullptr;
return m_childItems.at(row);
}
int RowNode::childCount() const
{
return m_childItems.count();
}
int RowNode::columnCount() const
{
return 6;
}
std::shared_ptr<Node> RowNode::parentItem()
{
std::shared_ptr<Node> temp = m_parentItem.lock();
if (!temp)
return nullptr;
return temp;
}
QVariant RowNode::data(int column)
{
if (column < 0 || column >= m_itemData.size())
return QVariant();
return m_itemData.at(column);
}
int RowNode::row() const
{
try {
std::shared_ptr<RowNode> temp = m_parentItem.lock();
if (temp) {
auto nodePtr = std::const_pointer_cast<Node>(shared_from_this());
return temp->m_childItems.indexOf(nodePtr);
}
} catch(...) {
return 0;
}
return 0;
}
std::shared_ptr<RowNode> RowNode::createNode(QMap<QString, QVariant> bookItem, QMap<QString, QByteArray> iconMap, std::shared_ptr<RowNode> rootNode)
{
auto faviconUrl = "https://" + bookItem["faviconUrl"].toString();
QString id = bookItem["id"].toString();
QByteArray bookIcon;
try {
auto book = KiwixApp::instance()->getLibrary()->getBookById(id);
std::string favicon;
auto item = book.getIllustration(48);
favicon = item->getData();
bookIcon = QByteArray::fromRawData(reinterpret_cast<const char*>(favicon.data()), favicon.size());
bookIcon.detach(); // deep copy
} catch (...) {
if (iconMap.contains(faviconUrl)) {
bookIcon = iconMap[faviconUrl];
}
}
std::weak_ptr<RowNode> weakRoot = rootNode;
auto rowNodePtr = std::shared_ptr<RowNode>(new
RowNode({bookIcon, bookItem["title"],
bookItem["date"],
QString::fromStdString(kiwix::beautifyFileSize(bookItem["size"].toULongLong())),
bookItem["tags"]
}, id, weakRoot));
std::weak_ptr<RowNode> weakRowNodePtr = rowNodePtr;
const auto descNodePtr = std::make_shared<DescriptionNode>(DescriptionNode(bookItem["description"].toString(), weakRowNodePtr));
rowNodePtr->appendChild(descNodePtr);
return rowNodePtr;
}
bool RowNode::isChild(Node *candidate)
{
if (!candidate)
return false;
for (auto item : m_childItems) {
if (candidate == item.get())
return true;
}
return false;
}

49
src/rownode.h Normal file
View File

@ -0,0 +1,49 @@
#ifndef ROWNODE_H
#define ROWNODE_H
#include "node.h"
#include <QList>
#include "contentmanagermodel.h"
#include <QIcon>
#include "kiwix/book.h"
struct DownloadInfo
{
double progress;
QString completedLength;
QString downloadSpeed;
bool paused;
};
class RowNode : public Node
{
public:
explicit RowNode(QList<QVariant> itemData, QString bookId, std::weak_ptr<RowNode> parentItem);
~RowNode();
std::shared_ptr<Node> parentItem() override;
std::shared_ptr<Node> child(int row);
void appendChild(std::shared_ptr<Node> child);
int childCount() const override;
int columnCount() const override;
QVariant data(int column) override;
int row() const override;
QString getBookId() const override { return m_bookId; }
void setIconData(QByteArray iconData) { m_itemData[0] = iconData; }
bool isDownloading() const { return m_isDownloading; }
void setDownloadInfo(DownloadInfo downloadInfo) { m_downloadInfo = downloadInfo; }
DownloadInfo getDownloadInfo() const { return m_downloadInfo; }
void setIsDownloading(bool val) { m_isDownloading = val; }
static std::shared_ptr<RowNode> createNode(QMap<QString, QVariant> bookItem, QMap<QString, QByteArray> iconMap, std::shared_ptr<RowNode> rootNode);
bool isChild(Node* candidate);
private:
QList<QVariant> m_itemData;
QList<std::shared_ptr<Node>> m_childItems;
std::weak_ptr<RowNode> m_parentItem;
QString m_bookId;
bool m_isDownloading = false;
DownloadInfo m_downloadInfo;
};
#endif // ROWNODE_H

View File

@ -113,7 +113,7 @@ void TabBar::setContentManagerView(ContentManagerView* view)
qInfo() << "add widget";
mp_stackedWidget->addWidget(view);
mp_stackedWidget->show();
int idx = addTab(QIcon(":/icons/library-icon.svg"), "");
int idx = addTab(QIcon(":/icons/kiwix-logo.svg"), "");
setTabButton(idx, RightSide, nullptr);
}

View File

@ -0,0 +1,52 @@
#include "thumbnaildownloader.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPixmap>
#include <QIcon>
ThumbnailDownloader::ThumbnailDownloader(QObject *parent)
{
connect(this, &ThumbnailDownloader::oneThumbnailDownloaded, [=]() {
if (m_urlPairList.size() != 0)
downloadOnePair(m_urlPairList.takeFirst());
else
m_isDownloading = false;
});
}
ThumbnailDownloader::~ThumbnailDownloader()
{
}
void ThumbnailDownloader::addDownload(QString url, QModelIndex index)
{
m_urlPairList.append({index, url});
if (!m_isDownloading)
startDownload();
}
void ThumbnailDownloader::startDownload()
{
if (m_urlPairList.size() == 0) {
m_isDownloading = false;
return;
}
m_isDownloading = true;
downloadOnePair(m_urlPairList.takeFirst());
}
void ThumbnailDownloader::downloadOnePair(QPair<QModelIndex, QString> urlPair)
{
QNetworkRequest req(urlPair.second);
auto reply = manager.get(req);
connect(reply, &QNetworkReply::finished, this, [=](){
fileDownloaded(reply, urlPair);
});
}
void ThumbnailDownloader::fileDownloaded(QNetworkReply *pReply, QPair<QModelIndex, QString> urlPair)
{
auto downloadedData = pReply->readAll();
emit oneThumbnailDownloaded(urlPair.first, urlPair.second, downloadedData);
pReply->deleteLater();
}

37
src/thumbnaildownloader.h Normal file
View File

@ -0,0 +1,37 @@
#ifndef THUMBNAILDOWNLOADER_H
#define THUMBNAILDOWNLOADER_H
#include <QObject>
#include <QQueue>
#include <QNetworkAccessManager>
#include <QIcon>
#include <QNetworkReply>
#include <QModelIndex>
class ThumbnailDownloader : public QObject
{
Q_OBJECT
public:
ThumbnailDownloader(QObject *parent = 0);
~ThumbnailDownloader();
void addDownload(QString url, QModelIndex index);
void startDownload();
void downloadOnePair(QPair<QModelIndex, QString> urlPair);
void clearQueue() { m_urlPairList.clear(); }
signals:
void oneThumbnailDownloaded(QModelIndex, QString, QByteArray);
private:
QQueue<QPair<QModelIndex, QString>> m_urlPairList;
QNetworkAccessManager manager;
bool m_isDownloading = false;
private slots:
void fileDownloaded(QNetworkReply *pReply, QPair<QModelIndex, QString> urlPair);
};
#endif // THUMBNAILDOWNLOADER_H

151
ui/kiwixconfirmbox.ui Normal file
View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>kiwixconfirmbox</class>
<widget class="QDialog" name="kiwixconfirmbox">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>368</width>
<height>166</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>474</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="confirmText">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Would you like to confirm doing xyz?</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="confirmTitle">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Confirm title</string>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>250</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="yesButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Yes</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Ok</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="noButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>No</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>