mirror of
https://github.com/kiwix/kiwix-desktop.git
synced 2025-09-22 11:37:56 -04:00
Merge pull request #138 from kiwix/split--contentManagerHtml
Split contentmanager.html
This commit is contained in:
commit
9a3a554e6f
@ -2,5 +2,7 @@
|
||||
<qresource prefix="/">
|
||||
<file>js/vue.js</file>
|
||||
<file>texts/_contentManager.html</file>
|
||||
<file>css/_contentManager.css</file>
|
||||
<file>js/_contentManager.js</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
149
resources/css/_contentManager.css
Normal file
149
resources/css/_contentManager.css
Normal file
@ -0,0 +1,149 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
#searchInput {
|
||||
background-image: url('qrc:///icons/search.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: left top;
|
||||
background-size: 40px 40px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
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%;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
display: none
|
||||
}
|
||||
summary {
|
||||
height: 64px;
|
||||
}
|
||||
.book {
|
||||
border-top: 1px solid #EEE;
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
button {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
}
|
||||
.tablerow button {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border: 0px;
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.tablerow button:hover {
|
||||
color: white;
|
||||
background: blue;
|
||||
}
|
||||
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 0 0 0;
|
||||
margin: 2px 0px 2px;
|
||||
}
|
||||
|
||||
.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); }
|
||||
}
|
173
resources/js/_contentManager.js
Normal file
173
resources/js/_contentManager.js
Normal file
@ -0,0 +1,173 @@
|
||||
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 createDict(keys, values) {
|
||||
var d = {}
|
||||
for(var i=0; i<keys.length; i++) {
|
||||
d[keys[i]] = values[i];
|
||||
}
|
||||
return d;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Vue.set(app.downloads, id, createDict(DOWNLOAD_KEYS, values));
|
||||
});
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
new QWebChannel(qt.webChannelTransport, function(channel) {
|
||||
contentManager = channel.objects.contentManager;
|
||||
app = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
contentManager: contentManager,
|
||||
displayedBooksNb: 20,
|
||||
books: [],
|
||||
downloads: {}
|
||||
},
|
||||
methods: {
|
||||
openBook : function(book) {
|
||||
contentManager.openBook(book.id, function() {});
|
||||
},
|
||||
downloadBook : function(book) {
|
||||
contentManager.downloadBook(book.id, function(did) {
|
||||
if (did.length == 0)
|
||||
return;
|
||||
book.downloadId = did;
|
||||
downloadUpdaters[book.id] = setInterval(function() { getDownloadInfo(book.id); }, 1000);
|
||||
});
|
||||
},
|
||||
eraseBook : function(book) {
|
||||
contentManager.eraseBook(book.id);
|
||||
},
|
||||
pauseBook : function(book) {
|
||||
contentManager.pauseBook(book.id);
|
||||
},
|
||||
resumeBook : function(book) {
|
||||
contentManager.resumeBook(book.id);
|
||||
},
|
||||
cancelBook : function(book) {
|
||||
contentManager.cancelBook(book.id);
|
||||
clearInterval(downloadUpdaters[book.id]);
|
||||
Vue.delete(app.downloads, 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;
|
||||
},
|
||||
niceBytes : niceBytes
|
||||
}
|
||||
});
|
||||
contentManager.booksChanged.connect(onBooksChanged);
|
||||
contentManager.pendingRequest.connect(displayLoadIcon);
|
||||
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 localElement = document.getElementsByClassName("local-option")[0];
|
||||
localElement.style.display = (book.path) ? "block" : "none";
|
||||
};
|
||||
|
||||
function setContextMenuPosition() {
|
||||
var menu = document.getElementById("menu");
|
||||
menu.style.left = `${mouseX}px`;
|
||||
menu.style.top = `${mouseY}px`;
|
||||
};
|
@ -3,334 +3,8 @@
|
||||
<head>
|
||||
<script src="qrc:///js/vue.js"></script>
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript">
|
||||
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 createDict(keys, values) {
|
||||
var d = {}
|
||||
for(var i=0; i<keys.length; i++) {
|
||||
d[keys[i]] = values[i];
|
||||
}
|
||||
return d;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Vue.set(app.downloads, id, createDict(DOWNLOAD_KEYS, values));
|
||||
});
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
new QWebChannel(qt.webChannelTransport, function(channel) {
|
||||
contentManager = channel.objects.contentManager;
|
||||
app = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
contentManager: contentManager,
|
||||
displayedBooksNb: 20,
|
||||
books: [],
|
||||
downloads: {}
|
||||
},
|
||||
methods: {
|
||||
openBook : function(book) {
|
||||
contentManager.openBook(book.id, function() {});
|
||||
},
|
||||
downloadBook : function(book) {
|
||||
contentManager.downloadBook(book.id, function(did) {
|
||||
if (did.length == 0)
|
||||
return;
|
||||
book.downloadId = did;
|
||||
downloadUpdaters[book.id] = setInterval(function() { getDownloadInfo(book.id); }, 1000);
|
||||
});
|
||||
},
|
||||
eraseBook : function(book) {
|
||||
contentManager.eraseBook(book.id);
|
||||
},
|
||||
pauseBook : function(book) {
|
||||
contentManager.pauseBook(book.id);
|
||||
},
|
||||
resumeBook : function(book) {
|
||||
contentManager.resumeBook(book.id);
|
||||
},
|
||||
cancelBook : function(book) {
|
||||
contentManager.cancelBook(book.id);
|
||||
clearInterval(downloadUpdaters[book.id]);
|
||||
Vue.delete(app.downloads, 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;
|
||||
},
|
||||
niceBytes : niceBytes
|
||||
}
|
||||
});
|
||||
contentManager.booksChanged.connect(onBooksChanged);
|
||||
contentManager.pendingRequest.connect(displayLoadIcon);
|
||||
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 localElement = document.getElementsByClassName("local-option")[0];
|
||||
localElement.style.display = (book.path) ? "block" : "none";
|
||||
};
|
||||
|
||||
function setContextMenuPosition() {
|
||||
var menu = document.getElementById("menu");
|
||||
menu.style.left = `${mouseX}px`;
|
||||
menu.style.top = `${mouseY}px`;
|
||||
};
|
||||
|
||||
</script>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
#searchInput {
|
||||
background-image: url('qrc:///icons/search.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: left top;
|
||||
background-size: 40px 40px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
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%;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
display: none
|
||||
}
|
||||
summary {
|
||||
height: 64px;
|
||||
}
|
||||
.book {
|
||||
border-top: 1px solid #EEE;
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
button {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
}
|
||||
.tablerow button {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border: 0px;
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.tablerow button:hover {
|
||||
color: white;
|
||||
background: blue;
|
||||
}
|
||||
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 0 0 0;
|
||||
margin: 2px 0px 2px;
|
||||
}
|
||||
|
||||
.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); }
|
||||
}
|
||||
|
||||
</style>
|
||||
<script src="qrc:///js/_contentManager.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="qrc:///css/_contentManager.css"/>
|
||||
</head>
|
||||
<body onload="init()">
|
||||
<div id="app">
|
||||
|
Loading…
x
Reference in New Issue
Block a user