Replace QWebEngineView with QTreeView for ContentManagerView

We'll be using a QTreeView to display elements of the library.
This commit is contained in:
Nikhil Tanwar 2023-06-22 10:52:26 +05:30
parent d9d06eaf71
commit 8f61b418f9
10 changed files with 5 additions and 11619 deletions

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 +0,0 @@
html, body {
padding: 0;
margin: 0;
height: 100%;
position: relative;
width: 100%;
overflow: hidden;
}
#app {
height: 100%;
position: relative;
}
*:focus {
outline: none;
}
#searchBar {
padding: 10px;
}
#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;
}
#bookTable {
position: relative;
height: calc(100% - 42px); /* 42px = 40px(height of #searchInput) + 2px(border) */
width: 100%;
}
#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%
}
.tablerow,
.header {
display: flex;
flex-direction: row;
width: 100%;
}
.header {
color: #555;
margin-top: 20px;
}
.tablecell{
flex-basis:20%;
font-family: sans-serif;
}
.sortable:hover {
cursor: pointer;
}
.sortableBold {
font-weight: bold;
}
i {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
.arrowUp {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
}
.arrowDown {
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
.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;
}

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

@ -56,6 +56,5 @@
<file>icons/new-tab-icon.svg</file>
<file>icons/library-icon.svg</file>
<file>icons/open-file.svg</file>
<file>js/tools.js</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

@ -21,8 +21,7 @@ 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();
mp_view->show();
setCurrentLanguage(QLocale().name().split("_").at(0));
connect(mp_library, &Library::booksChanged, this, [=]() {emit(this->booksChanged());});
connect(this, &ContentManager::filterParamsChanged, this, &ContentManager::updateLibrary);

View File

@ -1,29 +1,9 @@
#include "contentmanagerview.h"
#include <QFile>
#include <QWebEngineProfile>
#include "kiwixapp.h"
ContentManagerView::ContentManagerView(QWidget *parent)
: QWebEngineView(parent)
: QTreeView(parent)
{
QWebEnginePage* page = new QWebEnginePage(KiwixApp::instance()->getProfile(), this);
setPage(page);
page->setWebChannel(&m_webChannel);
setContextMenuPolicy( Qt::NoContextMenu );
}
void ContentManagerView::registerObject(const QString& id, QObject* object)
{
m_webChannel.registerObject(id, object);
}
void ContentManagerView::setHtml()
{
QFile contentFile(":texts/_contentManager.html");
contentFile.open(QIODevice::ReadOnly);
auto byteContent = contentFile.readAll();
contentFile.close();
QWebEngineView::setHtml(byteContent);
setSortingEnabled(true);
}

View File

@ -2,17 +2,13 @@
#define CONTENTMANAGERVIEW_H
#include <QWebEngineView>
#include <QWebChannel>
#include <QTreeView>
class ContentManagerView : public QWebEngineView
class ContentManagerView : public QTreeView
{
Q_OBJECT
public:
ContentManagerView(QWidget *parent = Q_NULLPTR);
void registerObject(const QString &id, QObject *object);
void setHtml();
private:
QWebChannel m_webChannel;
};
#endif // CONTENTMANAGERVIEW_H