Browse Source

WebUI: Add log viewer

The javascript implementation of multi-select menu is from the source
https://github.com/PhilippeMarcMeyer/vanillaSelectBox. It is licensed
under the MIT License. Some minor fixes is made to pass the lint.

Co-authored-by: brvphoenix <30111323+brvphoenix@users.noreply.github.com>
Co-authored-by: ttyS3 <ttys3.rust@gmail.com>

PR #18290.
adaptive-webui-19844
brvphoenix 2 years ago committed by GitHub
parent
commit
0d376e7fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .pre-commit-config.yaml
  2. 4
      AUTHORS
  3. 1
      src/webui/www/.prettierignore
  4. 1
      src/webui/www/.stylelintrc.json
  5. 271
      src/webui/www/private/css/lib/vanillaSelectBox.css
  6. 2
      src/webui/www/private/index.html
  7. 102
      src/webui/www/private/scripts/client.js
  8. 149
      src/webui/www/private/scripts/dynamicTable.js
  9. 1403
      src/webui/www/private/scripts/lib/vanillaSelectBox.js
  10. 427
      src/webui/www/private/views/log.html
  11. 7
      src/webui/www/private/views/logTabs.html
  12. 4
      src/webui/www/webui.qrc

3
.pre-commit-config.yaml

@ -34,6 +34,7 @@ repos: @@ -34,6 +34,7 @@ repos:
exclude: |
(?x)^(
compile_commands.json |
src/webui/www/private/css/lib/.* |
src/webui/www/private/scripts/lib/.*
)$
@ -43,6 +44,7 @@ repos: @@ -43,6 +44,7 @@ repos:
(?x)^(
compile_commands.json |
configure |
src/webui/www/private/css/lib/.* |
src/webui/www/private/scripts/lib/.*
)$
exclude_types:
@ -53,6 +55,7 @@ repos: @@ -53,6 +55,7 @@ repos:
name: Check trailing whitespaces
exclude: |
(?x)^(
src/webui/www/private/css/lib/.* |
src/webui/www/private/scripts/lib/.*
)$
exclude_types:

4
AUTHORS

@ -29,6 +29,10 @@ Code from other projects: @@ -29,6 +29,10 @@ Code from other projects:
copyright: Dan Haim <negativeiq@users.sourceforge.net>
license: BSD
* files src/webui/www/private/css/lib/vanillaSelectBox.css src/webui/www/private/scripts/lib/vanillaSelectBox.js
copyright: Philippe Meyer <pmg.meyer@gmail.com>
license: MIT
Images Authors:
* files: src/icons/qbittorrent-tray.svg
copyright: Provided by HVS <hvs linuxmail org> (raster first proposal) and Atif Afzal(@atfzl github) <atif5801@gmail.com> (vectorized and modified)

1
src/webui/www/.prettierignore

@ -0,0 +1 @@ @@ -0,0 +1 @@
private/css/lib/*.css

1
src/webui/www/.stylelintrc.json

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
"plugins": [
"stylelint-order"
],
"ignoreFiles": ["private/css/lib/*.css"],
"rules": {
"color-hex-length": null,
"comment-empty-line-before": null,

271
src/webui/www/private/css/lib/vanillaSelectBox.css

@ -0,0 +1,271 @@ @@ -0,0 +1,271 @@
.hidden-search {
display: none !important;
}
li[data-parent].closed{
display:none !important;
}
li[data-parent].open:not(.hidden-search){
display:block !important;
}
.vsb-menu{
cursor:pointer;
z-index:1000;
display:block;
visibility: hidden;
position:absolute;/*Don't change*/
border:1px solid #B2B2B2;
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0,0,0,.15);
box-shadow: 0 6px 12px rgba(0,0,0,.175);
border-radius:4px;
font-size : 11px;
}
.vsb-js-search-zone{
position:absolute;/*Don't change*/
z-index:1001;
width: 80%;
min-height:1.8em;
padding: 2px;
background-color: #fff;
}
.vsb-js-search-zone input{
border: 1px solid grey;
margin-left: 2px;
width: 96%;
border-radius: 4px;
height: 25px !important;
}
.vsb-main{
position: relative;/*Don't change*/
display: inline-block;
vertical-align: middle;
text-align:left;
}
.vsb-menu li:hover {
background: linear-gradient(#f5f5f5, #e8e8e8);
}
.vsb-menu ul{
user-select:none;
list-style:none;
white-space: nowrap;
margin:0px;
margin-top:4px;
padding-left:10px;
padding-right:10px;
padding-bottom:3px;
color: #333;
cursor:pointer;
overflow-y:auto;
}
li.disabled{
cursor:not-allowed;
opacity:0.3;
background-color: #999;
}
li.overflow{
cursor:not-allowed;
opacity:0.3;
background-color: #999;
}
li.short{
overflow:hidden;
text-overflow: ellipsis;
}
.vsb-main button{
min-width: 120px;
border-radius: 0;
width: 100%;
text-align: left;
z-index: 1;
color: #333;
background: white !important;
border: 1px solid #999 !important;
line-height:20px;
font-size:14px;
padding:6px 12px;
}
.vsb-main button.disabled{
cursor:not-allowed;
opacity:0.65;
}
.vsb-main .title {
margin-right: 6px;
user-select:none;
}
.vsb-main li:hover {
background: linear-gradient(#f5f5f5, #e8e8e8);
}
.vsb-main ul{
white-space: nowrap;
}
.vsb-menu li {
font-size: 14px;
background-color: #fff;
min-height:1.4em;
padding: 0.2em 2em 0.2em 1em;
}
.vsb-menu li.grouped-option b {
display: inline-block;
font-size: 15px;
margin-left:10px;
transform: translate(-18px);
}
.vsb-menu li.grouped-option.open span {
display: inline-block;
font-size: inherit;
margin-top:-2px;
height: 8px;
width: 8px;
transform: translate(-38px) rotate(45deg);
border-bottom: 3px solid black;
border-right: 3px solid black;
border-radius:2px;
}
.vsb-menu li.grouped-option.closed span {
display: inline-block;
font-size: inherit;
height: 8px;
width: 8px;
transform: translate(-38px) rotate(-45deg);
border-bottom: 3px solid black;
border-right: 3px solid black;
border-radius:2px;
}
.vsb-menu li.grouped-option i {
display: inline-block;
font-size: inherit;
float:left;
font-weight:bold;
margin-left:22px;
margin-right:2px;
height: 11px;
width: 8px;
border : 1px solid;
border-radius : 3px;
padding: 1px 3px 2px 3px;
margin-top:0px;
color:black;
}
.vsb-menu li.grouped-option.checked i::after {
content: "";
display: inline-block;
font-size: inherit;
color: #333;
float:left;
margin-left:0px;
display: inline-block;
transform: rotate(45deg);
height: 8px;
width: 5px;
border-bottom: 3px solid black;
border-right: 3px solid black;
}
.vsb-menu :not(.multi) li.active {
margin-left:7px;
}
.vsb-menu :not(.multi) li.active::before {
content: "";
display: inline-block;
font-size: inherit;
margin-left:-18px;
transform: rotate(45deg);
height: 10px;
width: 5px;
border-bottom: 3px solid black;
border-right: 3px solid black;
border-radius:2px;
}
.vsb-menu .multi li {
font-size: 14px;
background-color: #fff;
min-height:1.4em;
padding: 0.2em 2em 0.2em 26px;
}
.vsb-menu .multi li.grouped-option {
font-size: 15px;
padding-left: 5px;
}
.vsb-menu .multi li.grouped-option:hover {
font-weight: bold;
text-decoration: underline;
color:rgb(52, 31, 112);
}
.vsb-menu .multi li:not(.grouped-option)::before{
content: "";
display: inline-block;
font-size: inherit;
float:left;
font-weight:bold;
margin-left:-22px;
margin-right:2px;
border : 1px solid;
border-radius : 3px;
padding : 7px;
margin-top:0px;
color:black;
}
.vsb-menu .multi li:not(.grouped-option).active::after {
content: "";
display: inline-block;
font-size: inherit;
color: #333;
float:left;
margin-left:-18px;
display: inline-block;
transform: rotate(45deg);
margin-top:1px;
height: 8px;
width: 5px;
border-bottom: 3px solid black;
border-right: 3px solid black;
}
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
li[data-parent]{
padding-left: 50px !important;
}

2
src/webui/www/private/index.html

@ -77,6 +77,7 @@ @@ -77,6 +77,7 @@
<li><a id="speedInBrowserTitleBarLink"><img class="MyMenuIcon" src="images/checked-completed.svg" alt="QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" />QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="showSearchEngineLink"><img class="MyMenuIcon" src="images/checked-completed.svg" alt="QBT_TR(Search Engine)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" />QBT_TR(Search Engine)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="showRssReaderLink"><img class="MyMenuIcon" src="images/checked-completed.svg" alt="QBT_TR(RSS)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" />QBT_TR(RSS Reader)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="showLogViewerLink"><img class="MyMenuIcon" src="images/checked-completed.svg" alt="QBT_TR(Log)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" />QBT_TR(Log)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="StatisticsLink"><img class="MyMenuIcon" src="images/view-statistics.svg" alt="QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" />QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]</a></li>
</ul>
</li>
@ -116,6 +117,7 @@ @@ -116,6 +117,7 @@
<li id="transfersTabLink" class="selected"><a class="tab">QBT_TR(Transfers)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="searchTabLink"><a class="tab">QBT_TR(Search)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="rssTabLink"><a class="tab">QBT_TR(RSS)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="logTabLink"><a class="tab">QBT_TR(Execution Log)QBT_TR[CONTEXT=MainWindow]</a></li>
</ul>
<div class="clear"></div>
</div>

102
src/webui/www/private/scripts/client.js

@ -35,6 +35,7 @@ let serverSyncMainDataInterval = 1500; @@ -35,6 +35,7 @@ let serverSyncMainDataInterval = 1500;
let customSyncMainDataInterval = null;
let searchTabInitialized = false;
let rssTabInitialized = false;
let logTabInitialized = false;
let syncRequestInProgress = false;
@ -190,9 +191,21 @@ window.addEvent('load', function() { @@ -190,9 +191,21 @@ window.addEvent('load', function() {
$("rssTabColumn").addClass("invisible");
};
const buildLogTab = function() {
new MochaUI.Column({
id: 'logTabColumn',
placement: 'main',
width: null
});
// start off hidden
$('logTabColumn').addClass('invisible');
};
buildTransfersTab();
buildSearchTab();
buildRssTab();
buildLogTab();
MochaUI.initializeTabs('mainWindowTabsList');
setCategoryFilter = function(hash) {
@ -304,6 +317,7 @@ window.addEvent('load', function() { @@ -304,6 +317,7 @@ window.addEvent('load', function() {
// After showing/hiding the toolbar + status bar
let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false";
let showRssReader = LocalPreferences.get('show_rss_reader') !== "false";
let showLogViewer = LocalPreferences.get('show_log_viewer') === 'true';
// After Show Top Toolbar
MochaUI.Desktop.setDesktopSize();
@ -912,6 +926,12 @@ window.addEvent('load', function() { @@ -912,6 +926,12 @@ window.addEvent('load', function() {
updateTabDisplay();
});
$('showLogViewerLink').addEvent('click', function(e) {
showLogViewer = !showLogViewer;
LocalPreferences.set('show_log_viewer', showLogViewer.toString());
updateTabDisplay();
});
const updateTabDisplay = function() {
if (showRssReader) {
$('showRssReaderLink').firstChild.style.opacity = '1';
@ -941,8 +961,22 @@ window.addEvent('load', function() { @@ -941,8 +961,22 @@ window.addEvent('load', function() {
$("transfersTabLink").click();
}
if (showLogViewer) {
$('showLogViewerLink').firstChild.style.opacity = '1';
$('mainWindowTabs').removeClass('invisible');
$('logTabLink').removeClass('invisible');
if (!MochaUI.Panels.instances.LogPanel)
addLogPanel();
}
else {
$('showLogViewerLink').firstChild.style.opacity = '0';
$('logTabLink').addClass('invisible');
if ($('logTabLink').hasClass('selected'))
$("transfersTabLink").click();
}
// display no tabs
if (!showRssReader && !showSearchEngine)
if (!showRssReader && !showSearchEngine && !showLogViewer)
$('mainWindowTabs').addClass('invisible');
};
@ -954,18 +988,21 @@ window.addEvent('load', function() { @@ -954,18 +988,21 @@ window.addEvent('load', function() {
$("filtersColumn").removeClass("invisible");
$("filtersColumn_handle").removeClass("invisible");
$("mainColumn").removeClass("invisible");
$('torrentsFilterToolbar').removeClass("invisible");
customSyncMainDataInterval = null;
syncData(100);
hideSearchTab();
hideRssTab();
hideLogTab();
};
const hideTransfersTab = function() {
$("filtersColumn").addClass("invisible");
$("filtersColumn_handle").addClass("invisible");
$("mainColumn").addClass("invisible");
$('torrentsFilterToolbar').addClass("invisible");
MochaUI.Desktop.resizePanels();
};
@ -979,6 +1016,7 @@ window.addEvent('load', function() { @@ -979,6 +1016,7 @@ window.addEvent('load', function() {
customSyncMainDataInterval = 30000;
hideTransfersTab();
hideRssTab();
hideLogTab();
};
const hideSearchTab = function() {
@ -999,12 +1037,35 @@ window.addEvent('load', function() { @@ -999,12 +1037,35 @@ window.addEvent('load', function() {
customSyncMainDataInterval = 30000;
hideTransfersTab();
hideSearchTab();
hideLogTab();
};
const hideRssTab = function() {
$("rssTabColumn").addClass("invisible");
window.qBittorrent.Rss.unload();
window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
MochaUI.Desktop.resizePanels();
};
const showLogTab = function() {
if (!logTabInitialized) {
window.qBittorrent.Log.init();
logTabInitialized = true;
}
else {
window.qBittorrent.Log.load();
}
$('logTabColumn').removeClass('invisible');
customSyncMainDataInterval = 30000;
hideTransfersTab();
hideSearchTab();
hideRssTab();
};
const hideLogTab = function() {
$('logTabColumn').addClass('invisible');
MochaUI.Desktop.resizePanels();
window.qBittorrent.Log && window.qBittorrent.Log.unload();
};
const addSearchPanel = function() {
@ -1045,6 +1106,42 @@ window.addEvent('load', function() { @@ -1045,6 +1106,42 @@ window.addEvent('load', function() {
});
};
var addLogPanel = function() {
new MochaUI.Panel({
id: 'LogPanel',
title: 'Log',
header: true,
padding: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
loadMethod: 'xhr',
contentURL: 'views/log.html',
require: {
css: ['css/lib/vanillaSelectBox.css'],
js: ['scripts/lib/vanillaSelectBox.js'],
},
tabsURL: 'views/logTabs.html',
tabsOnload: function() {
MochaUI.initializeTabs('panelTabs');
$('logMessageLink').addEvent('click', function(e) {
window.qBittorrent.Log.setCurrentTab('main');
});
$('logPeerLink').addEvent('click', function(e) {
window.qBittorrent.Log.setCurrentTab('peer');
});
},
collapsible: false,
content: '',
column: 'logTabColumn',
height: null
});
};
new MochaUI.Panel({
id: 'transferList',
title: 'Panel',
@ -1185,6 +1282,7 @@ window.addEvent('load', function() { @@ -1185,6 +1282,7 @@ window.addEvent('load', function() {
$('transfersTabLink').addEvent('click', showTransfersTab);
$('searchTabLink').addEvent('click', showSearchTab);
$('rssTabLink').addEvent('click', showRssTab);
$('logTabLink').addEvent('click', showLogTab);
updateTabDisplay();
const registerDragAndDrop = () => {

149
src/webui/www/private/scripts/dynamicTable.js

@ -46,6 +46,8 @@ window.qBittorrent.DynamicTable = (function() { @@ -46,6 +46,8 @@ window.qBittorrent.DynamicTable = (function() {
SearchPluginsTable: SearchPluginsTable,
TorrentTrackersTable: TorrentTrackersTable,
TorrentFilesTable: TorrentFilesTable,
LogMessageTable: LogMessageTable,
LogPeerTable: LogPeerTable,
RssFeedTable: RssFeedTable,
RssArticleTable: RssArticleTable,
RssDownloaderRulesTable: RssDownloaderRulesTable,
@ -2610,6 +2612,153 @@ window.qBittorrent.DynamicTable = (function() { @@ -2610,6 +2612,153 @@ window.qBittorrent.DynamicTable = (function() {
}
});
const LogMessageTable = new Class({
Extends: DynamicTable,
filterText: '',
filterdLength: function() {
return this.tableBody.getElements('tr').length;
},
initColumns: function() {
this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]', 350, true);
this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
this.newColumn('type', '', 'QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]', 100, true);
this.initColumnsFunctions();
},
initColumnsFunctions: function() {
this.columns['timestamp'].updateTd = function(td, row) {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
td.set({ 'text': date, 'title': date });
};
this.columns['type'].updateTd = function(td, row) {
//Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
let logLevel, addClass;
switch (this.getRowValue(row).toInt()) {
case 1:
logLevel = 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'logNormal';
break;
case 2:
logLevel = 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'logInfo';
break;
case 4:
logLevel = 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'logWarning';
break;
case 8:
logLevel = 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'logCritical';
break;
default:
logLevel = 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'logUnknown';
break;
}
td.set({ 'text': logLevel, 'title': logLevel });
td.getParent('tr').set('class', 'logTableRow ' + addClass);
};
},
getFilteredAndSortedRows: function() {
let filteredRows = [];
const rows = this.rows.getValues();
this.filterText = window.qBittorrent.Log.getFilterText();
const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
const logLevels = window.qBittorrent.Log.getSelectedLevels();
if (filterTerms.length > 0 || logLevels.length < 4) {
for (let i = 0; i < rows.length; ++i) {
if (logLevels.indexOf(rows[i].full_data.type.toString()) == -1)
continue;
if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
continue;
filteredRows.push(rows[i]);
}
}
else {
filteredRows = rows;
}
filteredRows.sort(function(row1, row2) {
const column = this.columns[this.sortedColumn];
const res = column.compareRows(row1, row2);
return (this.reverseSort == '0') ? res : -res;
}.bind(this));
return filteredRows;
},
setupCommonEvents: function() {},
setupTr: function(tr) {
tr.addClass('logTableRow');
}
});
const LogPeerTable = new Class({
Extends: LogMessageTable,
initColumns: function() {
this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
this.newColumn('blocked', '', 'QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
this.newColumn('reason', '', 'QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
this.columns['timestamp'].updateTd = function(td, row) {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
td.set({ 'text': date, 'title': date });
};
this.columns['blocked'].updateTd = function(td, row) {
let status, addClass;
if (this.getRowValue(row)) {
status = 'QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'peerBlocked';
}
else {
status = 'QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]';
addClass = 'peerBanned';
}
td.set({ 'text': status, 'title': status });
td.getParent('tr').set('class', 'logTableRow ' + addClass);
};
},
getFilteredAndSortedRows: function() {
let filteredRows = [];
const rows = this.rows.getValues();
this.filterText = window.qBittorrent.Log.getFilterText();
const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
if (filterTerms.length > 0) {
for (let i = 0; i < rows.length; ++i) {
if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
continue;
filteredRows.push(rows[i]);
}
}
else {
filteredRows = rows;
}
filteredRows.sort(function(row1, row2) {
const column = this.columns[this.sortedColumn];
const res = column.compareRows(row1, row2);
return (this.reverseSort == '0') ? res : -res;
}.bind(this));
return filteredRows;
}
});
return exports();
})();

1403
src/webui/www/private/scripts/lib/vanillaSelectBox.js

File diff suppressed because it is too large Load Diff

427
src/webui/www/private/views/log.html

@ -0,0 +1,427 @@ @@ -0,0 +1,427 @@
<style type="text/css">
#logTopBar {
margin-top: 1em;
}
#logFilterBar {
margin: .5em 0;
}
#logFilterBar>label {
font-weight: bold;
margin-right: 10px;
}
#logFilterBar>button {
display: inline-block;
height: 24px;
padding: 0 .5em;
margin-left: .3em;
font-weight: bold;
}
#logView {
padding: 0 20px;
overflow: auto;
}
#logContentView {
display: block;
vertical-align: top;
}
#logMessageTableFixedHeaderDiv .dynamicTableHeader,
#logPeerTableFixedHeaderDiv .dynamicTableHeader {
cursor: default;
}
#filterTextInput {
background-image: url("../images/edit-find.svg");
background-repeat: no-repeat;
background-position: left;
background-size: 1.5em;
padding: 1px 5px 1px 2em;
margin-left: 20px;
width: 200px;
}
#logFilterSummary {
overflow: auto;
margin: 1em 0 .5em;
}
#numFilteredLogs,
#numTotalLogs {
font-style: italic;
}
.logNormal {
color: #80766e;
}
.logInfo {
color: #1781b5;
}
.logWarning {
color: #f97d1c;
}
.logCritical,
.peerBlocked {
color: #ee3f4d;
}
.vsb-main>button {
padding: 0 12px !important;
}
.contextMenu>li>a>img {
margin-right: 0.5em;
}
</style>
<div id="logView">
<div id="logTopBar">
<div id="logFilterBar">
<label for="logLevelSelect">QBT_TR(Log Levels:)QBT_TR[CONTEXT=ExecutionLogWidget]</label>
<select multiple size="1" id="logLevelSelect" class="logLevelSelect" onchange="window.qBittorrent.Log.logLevelChanged()">
<option value="1">QBT_TR(Normal Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
<option value="2">QBT_TR(Information Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
<option value="4">QBT_TR(Warning Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
<option value="8">QBT_TR(Critical Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
</select>
<input type="text" id="filterTextInput" onkeyup="window.qBittorrent.Log.filterTextChanged()" placeholder="QBT_TR(Filter logs)QBT_TR[CONTEXT=ExecutionLogWidget]" autocomplete="off" autocorrect="off" autocapitalize="none" />
<button title="Clear input" onclick="javascript:document.querySelector('#filterTextInput').value='';window.qBittorrent.Log.filterTextChanged();">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</button>
</div>
<div id="logFilterSummary">
<span>QBT_TR(Results)QBT_TR[CONTEXT=ExecutionLogWidget] (QBT_TR(showing)QBT_TR[CONTEXT=ExecutionLogWidget] <span id="numFilteredLogs">0</span> QBT_TR(out of)QBT_TR[CONTEXT=ExecutionLogWidget] <span id="numTotalLogs">0</span>):</span>
</div>
</div>
<div id="logContentView">
<div id="logMessageView">
<div id="logMessageTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable" style="position:relative;">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="logMessageTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="logPeerView" class="invisible">
<div id="logPeerTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable" style="position:relative;">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="logPeerTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<ul id="logTableMenu" class="contextMenu">
<li><a href="#" class="copyLogDataToClipboard"><img src="images/edit-copy.svg" alt="QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]" />QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
<li><a href="#Clear"><img src="images/list-remove.svg" alt="QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]" />QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
</ul>
<script>
'use strict';
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
window.qBittorrent.Log = (() => {
const exports = () => {
return {
init: init,
unload: unload,
load: load,
setCurrentTab: setCurrentTab,
getFilterText: getFilterText,
getSelectedLevels: getSelectedLevels,
logLevelChanged: logLevelChanged,
filterTextChanged: filterTextChanged
};
};
let currentSelectedTab = 'main';
let tableInfo = {
main: {
instance: new window.qBittorrent.DynamicTable.LogMessageTable(),
progress: false,
timer: null,
last_id: -1
},
peer: {
instance: new window.qBittorrent.DynamicTable.LogPeerTable(),
progress: false,
timer: null,
last_id: -1
}
};
let customSyncLogDataInterval = null;
let logFilterTimer;
let inputedFilterText = "";
let selectBox;
let selectedLogLevels = JSON.parse(LocalPreferences.get('qbt_selected_log_levels')) || ['1', '2', '4', '8'];
const init = () => {
$('logLevelSelect').getElements('option').each((x) => {
if (selectedLogLevels.indexOf(x.value.toString()) !== -1) {
x.setAttribute('selected', '');
}
else {
x.removeAttribute('selected');
}
});
selectBox = new vanillaSelectBox('#logLevelSelect', {
maxHeight: 200,
search: false,
translations: {
all: 'QBT_TR(All)QBT_TR[CONTEXT=ExecutionLogWidget]',
item: 'QBT_TR(item)QBT_TR[CONTEXT=ExecutionLogWidget]',
items: 'QBT_TR(items)QBT_TR[CONTEXT=ExecutionLogWidget]',
selectAll: 'QBT_TR(Select All)QBT_TR[CONTEXT=ExecutionLogWidget]',
clearAll: 'QBT_TR(Clear All)QBT_TR[CONTEXT=ExecutionLogWidget]',
},
placeHolder: "QBT_TR(Choose a log level...)QBT_TR[CONTEXT=ExecutionLogWidget]",
});
const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: '.logTableRow',
menu: 'logTableMenu',
actions: {
Clear: () => {
tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach(function(rowId) {
tableInfo[currentSelectedTab].instance.removeRow(rowId);
});
updateLableCount();
}
},
offsets: {
x: -16,
y: -57
}
});
tableInfo['main'].instance.setup('logMessageTableDiv', 'logMessageTableFixedHeaderDiv', logTableContextMenu);
tableInfo['peer'].instance.setup('logPeerTableDiv', 'logPeerTableFixedHeaderDiv', logTableContextMenu);
MUI.Panels.instances.LogPanel.contentEl.setStyle('height', '100%');
$('logView').setStyle('height', 'inherit');
load();
};
const unload = () => {
for (let table in tableInfo)
resetTableTimer(table);
};
const load = () => {
customSyncLogDataInterval = null;
syncLogWithInterval(100);
};
const resetTableTimer = (curTab) => {
if (curTab === undefined)
curTab = currentSelectedTab;
clearTimeout(tableInfo[curTab].timer);
tableInfo[curTab].timer = null;
};
const syncLogWithInterval = (interval) => {
if (!tableInfo[currentSelectedTab].progress) {
clearTimeout(tableInfo[currentSelectedTab].timer);
tableInfo[currentSelectedTab].timer = syncLogData.delay(interval, null, currentSelectedTab);
}
};
const getFilterText = () => {
return inputedFilterText;
};
const getSelectedLevels = () => {
return selectedLogLevels;
};
const getSyncLogDataInterval = () => {
return customSyncLogDataInterval ? customSyncLogDataInterval : serverSyncMainDataInterval;
};
const logLevelChanged = () => {
const value = selectBox.getResult().sort();
if (selectedLogLevels !== value) {
tableInfo[currentSelectedTab].last_id = -1;
selectedLogLevels = value;
LocalPreferences.set('qbt_selected_log_levels', JSON.stringify(selectedLogLevels));
logFilterChanged();
}
};
const filterTextChanged = () => {
const value = $('filterTextInput').get('value').trim();
if (inputedFilterText !== value) {
inputedFilterText = value;
logFilterChanged();
}
};
const logFilterChanged = () => {
clearTimeout(logFilterTimer);
logFilterTimer = setTimeout((curTab) => {
tableInfo[curTab].instance.updateTable(false);
updateLableCount(curTab);
}, 400, currentSelectedTab);
};
const setCurrentTab = (tab) => {
if (tab === currentSelectedTab)
return;
currentSelectedTab = tab;
if (currentSelectedTab === 'main') {
selectBox.enable();
$('logMessageView').removeClass('invisible');
$('logPeerView').addClass('invisible');
resetTableTimer('peer');
}
else {
selectBox.disable();
$('logMessageView').addClass('invisible');
$('logPeerView').removeClass('invisible');
resetTableTimer('main');
}
clearTimeout(logFilterTimer);
load();
if (tableInfo[currentSelectedTab].instance.filterText !== getFilterText()) {
tableInfo[currentSelectedTab].instance.updateTable();
}
updateLableCount();
};
const updateLableCount = (curTab) => {
if (curTab === undefined)
curTab = currentSelectedTab;
$('numFilteredLogs').set('text', tableInfo[curTab].instance.filterdLength());
$('numTotalLogs').set('text', tableInfo[curTab].instance.getRowIds().length);
};
const syncLogData = (curTab) => {
if (curTab === undefined)
curTab = currentSelectedTab;
let url;
if (curTab === 'main') {
url = new URI('api/v2/log/main');
url.setData({
normal: selectedLogLevels.indexOf('1') !== -1,
info: selectedLogLevels.indexOf('2') !== -1,
warning: selectedLogLevels.indexOf('4') !== -1,
critical: selectedLogLevels.indexOf('8') !== -1
});
}
else {
url = new URI('api/v2/log/peers');
}
url.setData('last_known_id', tableInfo[curTab].last_id);
tableInfo[curTab].progress = true;
new Request.JSON({
url: url,
noCache: true,
method: 'get',
onFailure: function(response) {
const errorDiv = $('error_div');
if (errorDiv)
errorDiv.set('text', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
tableInfo[curTab].progress = false;
syncLogWithInterval(10000);
},
onSuccess: function(response) {
$('error_div').set('text', '');
if ($('logTabColumn').hasClass('invisible'))
return;
if (response.length > 0) {
clearTimeout(logFilterTimer);
for (let i = 0; i < response.length; ++i) {
let row;
if (curTab === 'main') {
row = {
rowId: response[i].id,
message: response[i].message,
timestamp: response[i].timestamp,
type: response[i].type,
};
}
else {
row = {
rowId: response[i].id,
ip: response[i].ip,
timestamp: response[i].timestamp,
blocked: response[i].blocked,
reason: response[i].reason,
};
}
tableInfo[curTab].instance.updateRowData(row);
tableInfo[curTab].last_id = Math.max(response[i].id.toInt(), tableInfo[curTab].last_id);
}
tableInfo[curTab].instance.updateTable();
tableInfo[curTab].instance.altRow();
updateLableCount(curTab);
}
tableInfo[curTab].progress = false;
syncLogWithInterval(getSyncLogDataInterval());
}
}).send();
};
new ClipboardJS('.copyLogDataToClipboard', {
text: function() {
let msg = [];
tableInfo[currentSelectedTab].instance.selectedRowsIds().each(function(rowId) {
msg.push(tableInfo[currentSelectedTab].instance.rows.get(rowId).full_data[(currentSelectedTab === 'main') ? 'message' : 'ip']);
});
return msg.join('\n');
}
});
return exports();
})();
</script>

7
src/webui/www/private/views/logTabs.html

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<div class="toolbarTabs">
<ul id="panelTabs" class="tab-menu">
<li id="logMessageLink" class="selected"><a title="QBT_TR(General)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(General)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
<li id="logPeerLink"><a title="QBT_TR(Blocked IPs)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Blocked IPs)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
</ul>
<div class="clear"></div>
</div>

4
src/webui/www/webui.qrc

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
<file>private/css/noscript.css</file>
<file>private/css/style.css</file>
<file>private/css/Tabs.css</file>
<file>private/css/lib/vanillaSelectBox.css</file>
<file>private/css/Window.css</file>
<file>private/download.html</file>
<file>private/downloadlimit.html</file>
@ -392,6 +393,7 @@ @@ -392,6 +393,7 @@
<file>private/scripts/prop-trackers.js</file>
<file>private/scripts/prop-webseeds.js</file>
<file>private/scripts/speedslider.js</file>
<file>private/scripts/lib/vanillaSelectBox.js</file>
<file>private/setlocation.html</file>
<file>private/shareratio.html</file>
<file>private/upload.html</file>
@ -400,6 +402,8 @@ @@ -400,6 +402,8 @@
<file>private/views/aboutToolbar.html</file>
<file>private/views/filters.html</file>
<file>private/views/installsearchplugin.html</file>
<file>private/views/log.html</file>
<file>private/views/logTabs.html</file>
<file>private/views/preferences.html</file>
<file>private/views/preferencesToolbar.html</file>
<file>private/views/properties.html</file>

Loading…
Cancel
Save