Browse Source

Merge pull request #12557 from SeproDE/webui-rss

Implemented RSS reader and auto downloader in reference WebUI
adaptive-webui-19844
Mike Tzou 5 years ago committed by GitHub
parent
commit
17205802ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 55
      src/webui/www/private/confirmfeeddeletion.html
  2. 48
      src/webui/www/private/confirmruleclear.html
  3. 56
      src/webui/www/private/confirmruledeletion.html
  4. 1
      src/webui/www/private/css/Tabs.css
  5. 2
      src/webui/www/private/index.html
  6. 78
      src/webui/www/private/newfeed.html
  7. 77
      src/webui/www/private/newfolder.html
  8. 70
      src/webui/www/private/newrule.html
  9. 88
      src/webui/www/private/rename_feed.html
  10. 81
      src/webui/www/private/rename_rule.html
  11. 110
      src/webui/www/private/scripts/client.js
  12. 129
      src/webui/www/private/scripts/contextmenu.js
  13. 493
      src/webui/www/private/scripts/dynamicTable.js
  14. 64
      src/webui/www/private/views/preferences.html
  15. 5
      src/webui/www/private/views/preferencesToolbar.html
  16. 823
      src/webui/www/private/views/rss.html
  17. 735
      src/webui/www/private/views/rssDownloader.html
  18. 10
      src/webui/www/webui.qrc

55
src/webui/www/private/confirmfeeddeletion.html

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';
window.addEvent('domready', () => {
const paths = decodeURIComponent(new URI().getData('paths')).split('|');
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
new Event(e).stop();
window.parent.closeWindows();
});
$('confirmBtn').addEvent('click', (e) => {
new Event(e).stop();
let completionCount = 0;
paths.forEach((path) => {
new Request({
url: '/api/v2/rss/removeItem',
noCache: true,
method: 'post',
data: {
path: path
},
onComplete: (response) => {
++completionCount;
if (completionCount === paths.length) {
window.parent.qBittorrent.Rss.updateRssFeedList();
window.parent.closeWindows();
}
}
}).send();
});
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p>QBT_TR(Are you sure you want to delete the selected RSS feeds?)QBT_TR[CONTEXT=RSSWidget]</p>
<div style="text-align: right;">
<input type="button" id="cancelBtn" value="QBT_TR(No)QBT_TR[CONTEXT=MainWindow]" />
<input type="button" id="confirmBtn" value="QBT_TR(Yes)QBT_TR[CONTEXT=MainWindow]" />
</div>
</div>
</body>
</html>

48
src/webui/www/private/confirmruleclear.html

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Clear downloaded episodes)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';
window.addEvent('domready', () => {
const rules = decodeURIComponent(new URI().getData('rules')).split('|');
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
new Event(e).stop();
window.parent.MochaUI.closeWindow(window.parent.$('clearRulesPage'));
});
$('confirmBtn').addEvent('click', (e) => {
new Event(e).stop();
let completionCount = 0;
rules.forEach((rule) => {
window.parent.qBittorrent.RssDownloader.modifyRuleState(rule, 'previouslyMatchedEpisodes', [], () => {
++completionCount;
if (completionCount === rules.length) {
window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('clearRulesPage'));
}
});
});
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p>QBT_TR(Are you sure you want to clear the list of downloaded episodes for the selected rule?)QBT_TR[CONTEXT=AutomatedRssDownloader]</p>
<div style="text-align: right;">
<input type="button" id="cancelBtn" value="QBT_TR(No)QBT_TR[CONTEXT=MainWindow]" />
<input type="button" id="confirmBtn" value="QBT_TR(Yes)QBT_TR[CONTEXT=MainWindow]" />
</div>
</div>
</body>
</html>

56
src/webui/www/private/confirmruledeletion.html

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Rule deletion confirmation)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';
window.addEvent('domready', () => {
const rules = decodeURIComponent(new URI().getData('rules')).split('|');
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
new Event(e).stop();
window.parent.MochaUI.closeWindow(window.parent.$('removeRulePage'));
});
$('confirmBtn').addEvent('click', (e) => {
new Event(e).stop();
let completionCount = 0;
rules.forEach((rule) => {
new Request({
url: '/api/v2/rss/removeRule',
noCache: true,
method: 'post',
data: {
ruleName: rule
},
onComplete: (response) => {
++completionCount;
if (completionCount === rules.length) {
window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('removeRulePage'));
}
}
}).send();
});
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p>QBT_TR(Are you sure you want to remove the selected download rules?)QBT_TR[CONTEXT=AutomatedRssDownloader]</p>
<div style="text-align: right;">
<input type="button" id="cancelBtn" value="QBT_TR(No)QBT_TR[CONTEXT=MainWindow]" />
<input type="button" id="confirmBtn" value="QBT_TR(Yes)QBT_TR[CONTEXT=MainWindow]" />
</div>
</div>
</body>
</html>

1
src/webui/www/private/css/Tabs.css

@ -34,7 +34,6 @@ Required by:
} }
.tab-menu li { .tab-menu li {
display: block;
float: left; float: left;
margin: 0 0 5px 0; margin: 0 0 5px 0;
cursor: pointer; cursor: pointer;

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

@ -74,6 +74,7 @@
<li><a id="showStatusBarLink"><img class="MyMenuIcon" src="icons/checked.svg" alt="QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]</a></li> <li><a id="showStatusBarLink"><img class="MyMenuIcon" src="icons/checked.svg" alt="QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Status Bar)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="speedInBrowserTitleBarLink"><img class="MyMenuIcon" src="icons/checked.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><a id="speedInBrowserTitleBarLink"><img class="MyMenuIcon" src="icons/checked.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="icons/checked.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 class="divider"><a id="showSearchEngineLink"><img class="MyMenuIcon" src="icons/checked.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="icons/checked.svg" alt="QBT_TR(RSS)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(RSS Reader)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="StatisticsLink"><img class="MyMenuIcon" src="icons/view-statistics.svg" alt="QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]" width="16" height="16"/>QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]</a></li> <li class="divider"><a id="StatisticsLink"><img class="MyMenuIcon" src="icons/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> </ul>
</li> </li>
@ -112,6 +113,7 @@
<ul id="mainWindowTabsList" class="tab-menu"> <ul id="mainWindowTabsList" class="tab-menu">
<li id="transfersTabLink" class="selected"><a class="tab">QBT_TR(Transfers)QBT_TR[CONTEXT=MainWindow]</a></li> <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="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>
</ul> </ul>
<div class="clear"></div> <div class="clear"></div>
</div> </div>

78
src/webui/www/private/newfeed.html

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
new Keyboard({
defaultEventType: 'keydown',
events: {
'Enter': (event) => {
$('submitButton').click();
event.preventDefault();
},
'Escape': (event) => {
window.parent.closeWindows();
event.preventDefault();
},
'Esc': (event) => {
window.parent.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', () => {
$('feedURL').focus();
const path = decodeURIComponent(new URI().getData('path'));
$('submitButton').addEvent('click', (e) => {
new Event(e).stop();
// check field
const feedURL = $('feedURL').value.trim();
if (feedURL === '') {
alert('QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]');
return;
}
$('submitButton').disabled = true;
new Request({
url: '/api/v2/rss/addFeed',
noCache: true,
method: 'post',
data: {
url: feedURL,
path: path ? (path + '\\' + feedURL) : ''
},
onSuccess: (response) => {
window.parent.qBittorrent.Rss.updateRssFeedList();
window.parent.closeWindows();
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
$('submitButton').disabled = false;
}
}).send();
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p style="font-weight: bold;">QBT_TR(Feed URL:)QBT_TR[CONTEXT=RSSWidget]</p>
<input type="text" id="feedURL" value="" maxlength="100" style="width: 320px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(OK)QBT_TR[CONTEXT=HttpServer]" id="submitButton" />
</div>
</div>
</body>
</html>

77
src/webui/www/private/newfolder.html

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
new Keyboard({
defaultEventType: 'keydown',
events: {
'Enter': (event) => {
$('submitButton').click();
event.preventDefault();
},
'Escape': (event) => {
window.parent.closeWindows();
event.preventDefault();
},
'Esc': (event) => {
window.parent.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', () => {
$('folderName').focus();
const path = decodeURIComponent(new URI().getData('path'));
$('submitButton').addEvent('click', (e) => {
new Event(e).stop();
// check field
const folderName = $('folderName').value.trim();
if (folderName === '') {
alert('QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]');
return;
}
$('submitButton').disabled = true;
new Request({
url: '/api/v2/rss/addFolder',
noCache: true,
method: 'post',
data: {
path: path ? (path + '\\' + folderName) : folderName
},
onSuccess: (response) => {
window.parent.qBittorrent.Rss.updateRssFeedList();
window.parent.closeWindows();
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
$('submitButton').disabled = false;
}
}).send();
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p style="font-weight: bold;">QBT_TR(Folder name:)QBT_TR[CONTEXT=RSSWidget]</p>
<input type="text" id="folderName" value="" maxlength="100" style="width: 320px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(OK)QBT_TR[CONTEXT=HttpServer]" id="submitButton" />
</div>
</div>
</body>
</html>

70
src/webui/www/private/newrule.html

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(New rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
new Keyboard({
defaultEventType: 'keydown',
events: {
'Enter': (event) => {
$('submitButton').click();
event.preventDefault();
},
'Escape': (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage'));
event.preventDefault();
},
'Esc': (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage'));
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', () => {
$('name').focus();
$('submitButton').addEvent('click', (e) => {
new Event(e).stop();
// check field
const name = $('name').value.trim();
if (name === '') {
alert('QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]');
return;
}
$('submitButton').disabled = true;
new Request({
url: '/api/v2/rss/setRule',
noCache: true,
method: 'post',
data: {
ruleName: name,
ruleDef: '{}'
},
onSuccess: (response) => {
window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage'));
}
}).send();
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p>QBT_TR(Please type the name of the new download rule.)QBT_TR[CONTEXT=AutomatedRssDownloader]</p>
<input type="text" id="name" value="" maxlength="100" style="width: 320px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(OK)QBT_TR[CONTEXT=HttpServer]" id="submitButton" />
</div>
</div>
</body>
</html>

88
src/webui/www/private/rename_feed.html

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
new Keyboard({
defaultEventType: 'keydown',
events: {
'Enter': (event) => {
$('renameButton').click();
event.preventDefault();
},
'Escape': (event) => {
window.parent.closeWindows();
event.preventDefault();
},
'Esc': (event) => {
window.parent.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', () => {
const oldPath = decodeURIComponent(new URI().getData('oldPath'));
$('rename').value = oldPath;
$('rename').focus();
$('rename').setSelectionRange(0, oldPath.length);
$('renameButton').addEvent('click', (e) => {
new Event(e).stop();
// check field
const newPath = $('rename').value.trim();
if (newPath === '') {
alert('QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]');
return;
}
if (newPath === oldPath) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]');
return;
}
$('renameButton').disabled = true;
new Request({
url: '/api/v2/rss/moveItem',
noCache: true,
method: 'post',
data: {
itemPath: oldPath,
destPath: newPath
},
onSuccess: (response) => {
window.parent.qBittorrent.Rss.updateRssFeedList();
window.parent.closeWindows();
},
onFailure: (response) => {
if (response.status === 409) {
alert(response.responseText);
}
$('renameButton').disabled = false;
}
}).send();
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p style="font-weight: bold;">QBT_TR(New feed name:)QBT_TR[CONTEXT=RSSWidget]</p>
<input type="text" id="rename" value="" maxlength="100" style="width: 320px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" id="renameButton" />
</div>
</div>
</body>
</html>

81
src/webui/www/private/rename_rule.html

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Rule renaming)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
new Keyboard({
defaultEventType: 'keydown',
events: {
'Enter': (event) => {
$('renameButton').click();
event.preventDefault();
},
'Escape': (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage'));
event.preventDefault();
},
'Esc': (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage'));
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', () => {
const oldName = decodeURIComponent(new URI().getData('rule'));
$('rename').value = oldName;
$('rename').focus();
$('rename').setSelectionRange(0, oldName.length);
$('renameButton').addEvent('click', (e) => {
new Event(e).stop();
// check field
const newName = $('rename').value.trim();
if (newName === '') {
alert('QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]');
return;
}
if (newName === oldName) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]');
return;
}
$('renameButton').disabled = true;
new Request({
url: '/api/v2/rss/renameRule',
noCache: true,
method: 'post',
data: {
ruleName: oldName,
newRuleName: newName
},
onSuccess: (response) => {
window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage'));
}
}).send();
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p>QBT_TR(Please type the new rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]</p>
<input type="text" id="rename" value="" maxlength="100" style="width: 320px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" id="renameButton" />
</div>
</div>
</body>
</html>

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

@ -34,6 +34,7 @@ let queueing_enabled = true;
let serverSyncMainDataInterval = 1500; let serverSyncMainDataInterval = 1500;
let customSyncMainDataInterval = null; let customSyncMainDataInterval = null;
let searchTabInitialized = false; let searchTabInitialized = false;
let rssTabInitialized = false;
let syncRequestInProgress = false; let syncRequestInProgress = false;
@ -171,8 +172,20 @@ window.addEvent('load', function() {
$("searchTabColumn").addClass("invisible"); $("searchTabColumn").addClass("invisible");
}; };
const buildRssTab = function() {
new MochaUI.Column({
id: 'rssTabColumn',
placement: 'main',
width: null
});
// start off hidden
$("rssTabColumn").addClass("invisible");
};
buildTransfersTab(); buildTransfersTab();
buildSearchTab(); buildSearchTab();
buildRssTab();
MochaUI.initializeTabs('mainWindowTabsList'); MochaUI.initializeTabs('mainWindowTabsList');
setCategoryFilter = function(hash) { setCategoryFilter = function(hash) {
@ -275,12 +288,7 @@ window.addEvent('load', function() {
// After showing/hiding the toolbar + status bar // After showing/hiding the toolbar + status bar
let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false"; let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false";
if (!showSearchEngine) { let showRssReader = LocalPreferences.get('show_rss_reader') !== "false";
// uncheck menu option
$('showSearchEngineLink').firstChild.style.opacity = '0';
// hide tabs
$('mainWindowTabs').addClass('invisible');
}
// After Show Top Toolbar // After Show Top Toolbar
MochaUI.Desktop.setDesktopSize(); MochaUI.Desktop.setDesktopSize();
@ -860,22 +868,48 @@ window.addEvent('load', function() {
$('showSearchEngineLink').addEvent('click', function(e) { $('showSearchEngineLink').addEvent('click', function(e) {
showSearchEngine = !showSearchEngine; showSearchEngine = !showSearchEngine;
LocalPreferences.set('show_search_engine', showSearchEngine.toString()); LocalPreferences.set('show_search_engine', showSearchEngine.toString());
updateTabDisplay();
});
$('showRssReaderLink').addEvent('click', function(e) {
showRssReader = !showRssReader;
LocalPreferences.set('show_rss_reader', showRssReader.toString());
updateTabDisplay();
});
const updateTabDisplay = function() {
if (showRssReader) {
$('showRssReaderLink').firstChild.style.opacity = '1';
$('mainWindowTabs').removeClass('invisible');
$('rssTabLink').removeClass('invisible');
if (!MochaUI.Panels.instances.RssPanel)
addRssPanel();
}
else {
$('showRssReaderLink').firstChild.style.opacity = '0';
$('rssTabLink').addClass('invisible');
if ($('rssTabLink').hasClass('selected'))
$("transfersTabLink").click();
}
if (showSearchEngine) { if (showSearchEngine) {
$('showSearchEngineLink').firstChild.style.opacity = '1'; $('showSearchEngineLink').firstChild.style.opacity = '1';
$('mainWindowTabs').removeClass('invisible'); $('mainWindowTabs').removeClass('invisible');
$('searchTabLink').removeClass('invisible');
addMainWindowTabsEventListener();
if (!MochaUI.Panels.instances.SearchPanel) if (!MochaUI.Panels.instances.SearchPanel)
addSearchPanel(); addSearchPanel();
} }
else { else {
$('showSearchEngineLink').firstChild.style.opacity = '0'; $('showSearchEngineLink').firstChild.style.opacity = '0';
$('mainWindowTabs').addClass('invisible'); $('searchTabLink').addClass('invisible');
if ($('searchTabLink').hasClass('selected'))
$("transfersTabLink").click(); $("transfersTabLink").click();
removeMainWindowTabsEventListener();
} }
});
// display no tabs
if (!showRssReader && !showSearchEngine)
$('mainWindowTabs').addClass('invisible');
};
$('StatisticsLink').addEvent('click', StatisticsLinkFN); $('StatisticsLink').addEvent('click', StatisticsLinkFN);
@ -890,6 +924,7 @@ window.addEvent('load', function() {
syncData(100); syncData(100);
hideSearchTab(); hideSearchTab();
hideRssTab();
}; };
const hideTransfersTab = function() { const hideTransfersTab = function() {
@ -908,6 +943,7 @@ window.addEvent('load', function() {
$("searchTabColumn").removeClass("invisible"); $("searchTabColumn").removeClass("invisible");
customSyncMainDataInterval = 30000; customSyncMainDataInterval = 30000;
hideTransfersTab(); hideTransfersTab();
hideRssTab();
}; };
const hideSearchTab = function() { const hideSearchTab = function() {
@ -915,14 +951,25 @@ window.addEvent('load', function() {
MochaUI.Desktop.resizePanels(); MochaUI.Desktop.resizePanels();
}; };
const addMainWindowTabsEventListener = function() { const showRssTab = function() {
$('transfersTabLink').addEvent('click', showTransfersTab); if (!rssTabInitialized) {
$('searchTabLink').addEvent('click', showSearchTab); window.qBittorrent.Rss.init();
rssTabInitialized = true;
}
else {
window.qBittorrent.Rss.load();
}
$("rssTabColumn").removeClass("invisible");
customSyncMainDataInterval = 30000;
hideTransfersTab();
hideSearchTab();
}; };
const removeMainWindowTabsEventListener = function() { const hideRssTab = function() {
$('transfersTabLink').removeEvent('click', showTransfersTab); $("rssTabColumn").addClass("invisible");
$('searchTabLink').removeEvent('click', showSearchTab); window.qBittorrent.Rss.unload();
MochaUI.Desktop.resizePanels();
}; };
const addSearchPanel = function() { const addSearchPanel = function() {
@ -944,6 +991,25 @@ window.addEvent('load', function() {
}); });
}; };
const addRssPanel = function() {
new MochaUI.Panel({
id: 'RssPanel',
title: 'Rss',
header: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
loadMethod: 'xhr',
contentURL: 'views/rss.html',
content: '',
column: 'rssTabColumn',
height: null
});
};
new MochaUI.Panel({ new MochaUI.Panel({
id: 'transferList', id: 'transferList',
title: 'Panel', title: 'Panel',
@ -1081,10 +1147,10 @@ window.addEvent('load', function() {
} }
}); });
if (showSearchEngine) { $('transfersTabLink').addEvent('click', showTransfersTab);
addMainWindowTabsEventListener(); $('searchTabLink').addEvent('click', showSearchTab);
addSearchPanel(); $('rssTabLink').addEvent('click', showRssTab);
} updateTabDisplay();
}); });
function registerMagnetHandler() { function registerMagnetHandler() {

129
src/webui/www/private/scripts/contextmenu.js

@ -39,7 +39,10 @@ window.qBittorrent.ContextMenu = (function() {
TorrentsTableContextMenu: TorrentsTableContextMenu, TorrentsTableContextMenu: TorrentsTableContextMenu,
CategoriesFilterContextMenu: CategoriesFilterContextMenu, CategoriesFilterContextMenu: CategoriesFilterContextMenu,
TagsFilterContextMenu: TagsFilterContextMenu, TagsFilterContextMenu: TagsFilterContextMenu,
SearchPluginsTableContextMenu: SearchPluginsTableContextMenu SearchPluginsTableContextMenu: SearchPluginsTableContextMenu,
RssFeedContextMenu: RssFeedContextMenu,
RssArticleContextMenu: RssArticleContextMenu,
RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu
}; };
}; };
@ -538,5 +541,129 @@ window.qBittorrent.ContextMenu = (function() {
} }
}); });
const RssFeedContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
let selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
this.menu.getElement('a[href$=newSubscription]').parentNode.addClass('separator');
switch (selectedRows.length) {
case 0:
// remove seperator on top of newSubscription entry to avoid double line
this.menu.getElement('a[href$=newSubscription]').parentNode.removeClass('separator');
// menu when nothing selected
this.hideItem('update');
this.hideItem('markRead');
this.hideItem('rename');
this.hideItem('delete');
this.showItem('newSubscription');
this.showItem('newFolder');
this.showItem('updateAll');
this.hideItem('copyFeedURL');
break;
case 1:
if (selectedRows[0] === 0) {
// menu when "unread" feed selected
this.showItem('update');
this.showItem('markRead');
this.hideItem('rename');
this.hideItem('delete');
this.showItem('newSubscription');
this.hideItem('newFolder');
this.hideItem('updateAll');
this.hideItem('copyFeedURL');
}
else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === '') {
// menu when single folder selected
this.showItem('update');
this.showItem('markRead');
this.showItem('rename');
this.showItem('delete');
this.showItem('newSubscription');
this.showItem('newFolder');
this.hideItem('updateAll');
this.hideItem('copyFeedURL');
}
else {
// menu when single feed selected
this.showItem('update');
this.showItem('markRead');
this.showItem('rename');
this.showItem('delete');
this.showItem('newSubscription');
this.hideItem('newFolder');
this.hideItem('updateAll');
this.showItem('copyFeedURL');
}
break;
default:
// menu when multiple items selected
this.showItem('update');
this.showItem('markRead');
this.hideItem('rename');
this.showItem('delete');
this.hideItem('newSubscription');
this.hideItem('newFolder');
this.hideItem('updateAll');
this.showItem('copyFeedURL');
}
}
});
const RssArticleContextMenu = new Class({
Extends: ContextMenu
});
const RssDownloaderRuleContextMenu = new Class({
Extends: ContextMenu,
adjustMenuPosition: function(e) {
this.updateMenuItems();
// draw the menu off-screen to know the menu dimensions
this.menu.setStyles({
left: '-999em',
top: '-999em'
});
// position the menu
let xPosMenu = e.page.x + this.options.offsets.x - $('rssdownloaderpage').offsetLeft;
let yPosMenu = e.page.y + this.options.offsets.y - $('rssdownloaderpage').offsetTop;
if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
xPosMenu -= this.menu.offsetWidth;
if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
xPosMenu = Math.max(xPosMenu, 0);
yPosMenu = Math.max(yPosMenu, 0);
this.menu.setStyles({
left: xPosMenu,
top: yPosMenu,
position: 'absolute',
'z-index': '2000'
});
},
updateMenuItems: function() {
let selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
this.showItem('addRule');
switch (selectedRows.length) {
case 0:
// menu when nothing selected
this.hideItem('deleteRule');
this.hideItem('renameRule');
this.hideItem('clearDownloadedEpisodes');
break;
case 1:
// menu when single item selected
this.showItem('deleteRule');
this.showItem('renameRule');
this.showItem('clearDownloadedEpisodes');
break;
default:
// menu when multiple items selected
this.showItem('deleteRule');
this.hideItem('renameRule');
this.showItem('clearDownloadedEpisodes');
}
}
});
return exports(); return exports();
})(); })();

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

@ -45,7 +45,12 @@ window.qBittorrent.DynamicTable = (function() {
SearchResultsTable: SearchResultsTable, SearchResultsTable: SearchResultsTable,
SearchPluginsTable: SearchPluginsTable, SearchPluginsTable: SearchPluginsTable,
TorrentTrackersTable: TorrentTrackersTable, TorrentTrackersTable: TorrentTrackersTable,
TorrentFilesTable: TorrentFilesTable TorrentFilesTable: TorrentFilesTable,
RssFeedTable: RssFeedTable,
RssArticleTable: RssArticleTable,
RssDownloaderRulesTable: RssDownloaderRulesTable,
RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable,
RssDownloaderArticlesTable: RssDownloaderArticlesTable
}; };
}; };
@ -2000,6 +2005,492 @@ window.qBittorrent.DynamicTable = (function() {
} }
}); });
const RssFeedTable = new Class({
Extends: DynamicTable,
initColumns: function() {
this.newColumn('state_icon', '', '', 30, true);
this.newColumn('name', '', 'QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]', -1, true);
this.columns['state_icon'].dataProperties[0] = '';
// map name row to "[name] ([unread])"
this.columns['name'].dataProperties.push('unread');
this.columns['name'].updateTd = function(td, row) {
const name = this.getRowValue(row, 0);
const unreadCount = this.getRowValue(row, 1);
let value = name + ' (' + unreadCount + ')';
td.set('text', value);
td.set('title', value);
};
},
setupHeaderMenu: function() {},
setupHeaderEvents: function() {},
getFilteredAndSortedRows: function() {
return this.rows.getValues();
},
selectRow: function(rowId) {
this.selectedRows.push(rowId);
this.setRowClass();
this.onSelectedRowChanged();
const rows = this.rows.getValues();
let path = '';
for (let i = 0; i < rows.length; ++i) {
if (rows[i].rowId === rowId) {
path = rows[i].full_data.dataPath;
break;
}
}
window.qBittorrent.Rss.showRssFeed(path);
},
setupTr: function(tr) {
tr.addEvent('dblclick', function(e) {
if (this.rowId !== 0) {
window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
return true;
}
});
},
updateRow: function(tr, fullUpdate) {
const row = this.rows.get(tr.rowId);
const data = row[fullUpdate ? 'full_data' : 'data'];
const tds = tr.getElements('td');
for (let i = 0; i < this.columns.length; ++i) {
if (data.hasOwnProperty(this.columns[i].dataProperties[0]))
this.columns[i].updateTd(tds[i], row);
}
row['data'] = {};
tds[0].style.overflow = 'visible';
let indentaion = row.full_data.indentaion;
tds[0].style.paddingLeft = (indentaion * 32 + 4) + 'px';
tds[1].style.paddingLeft = (indentaion * 32 + 4) + 'px';
},
updateIcons: function() {
// state_icon
this.rows.each(row => {
let img_path;
switch (row.full_data.status) {
case 'default':
img_path = 'icons/application-rss+xml.svg';
break;
case 'hasError':
img_path = 'icons/unavailable.svg';
break;
case 'isLoading':
img_path = 'images/spinner.gif';
break;
case 'unread':
img_path = 'icons/mail-folder-inbox.svg';
break;
case 'isFolder':
img_path = 'icons/folder-documents.svg';
break;
}
let td;
for (let i = 0; i < this.tableBody.rows.length; ++i) {
if (this.tableBody.rows[i].rowId === row.rowId) {
td = this.tableBody.rows[i].children[0];
break;
}
}
if (td.getChildren('img').length > 0) {
const img = td.getChildren('img')[0];
if (img.src.indexOf(img_path) < 0) {
img.set('src', img_path);
img.set('title', status);
}
}
else {
td.adopt(new Element('img', {
'src': img_path,
'class': 'stateIcon',
'height': '22px',
'width': '22px'
}));
}
});
},
newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
const column = {};
column['name'] = name;
column['title'] = name;
column['visible'] = defaultVisible;
column['force_hide'] = false;
column['caption'] = caption;
column['style'] = style;
if (defaultWidth !== -1) {
column['width'] = defaultWidth;
}
column['dataProperties'] = [name];
column['getRowValue'] = function(row, pos) {
if (pos === undefined)
pos = 0;
return row['full_data'][this.dataProperties[pos]];
};
column['compareRows'] = function(row1, row2) {
if (this.getRowValue(row1) < this.getRowValue(row2))
return -1;
else if (this.getRowValue(row1) > this.getRowValue(row2))
return 1;
else return 0;
};
column['updateTd'] = function(td, row) {
const value = this.getRowValue(row)
td.set('text', value);
td.set('title', value);
};
column['onResize'] = null;
this.columns.push(column);
this.columns[name] = column;
this.hiddenTableHeader.appendChild(new Element('th'));
this.fixedTableHeader.appendChild(new Element('th'));
},
setupCommonEvents: function() {
const scrollFn = function() {
$(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
}.bind(this);
$(this.dynamicTableDivId).addEvent('scroll', scrollFn);
}
});
const RssArticleTable = new Class({
Extends: DynamicTable,
initColumns: function() {
this.newColumn('name', '', 'QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]', -1, true);
},
setupHeaderMenu: function() {},
setupHeaderEvents: function() {},
getFilteredAndSortedRows: function() {
return this.rows.getValues();
},
selectRow: function(rowId) {
this.selectedRows.push(rowId);
this.setRowClass();
this.onSelectedRowChanged();
const rows = this.rows.getValues();
let articleId = '';
let feedUid = '';
for (let i = 0; i < rows.length; ++i) {
if (rows[i].rowId === rowId) {
articleId = rows[i].full_data.dataId;
feedUid = rows[i].full_data.feedUid;
this.tableBody.rows[rows[i].rowId].removeClass('unreadArticle');
break;
}
}
window.qBittorrent.Rss.showDetails(feedUid, articleId);
},
setupTr: function(tr) {
tr.addEvent('dblclick', function(e) {
showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
return true;
});
tr.addClass('torrentsTableContextMenuTarget');
},
updateRow: function(tr, fullUpdate) {
const row = this.rows.get(tr.rowId);
const data = row[fullUpdate ? 'full_data' : 'data'];
if (!row.full_data.isRead)
tr.addClass('unreadArticle');
else
tr.removeClass('unreadArticle');
const tds = tr.getElements('td');
for (let i = 0; i < this.columns.length; ++i) {
if (data.hasOwnProperty(this.columns[i].dataProperties[0]))
this.columns[i].updateTd(tds[i], row);
}
row['data'] = {};
},
newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
const column = {};
column['name'] = name;
column['title'] = name;
column['visible'] = defaultVisible;
column['force_hide'] = false;
column['caption'] = caption;
column['style'] = style;
if (defaultWidth !== -1) {
column['width'] = defaultWidth;
}
column['dataProperties'] = [name];
column['getRowValue'] = function(row, pos) {
if (pos === undefined)
pos = 0;
return row['full_data'][this.dataProperties[pos]];
};
column['compareRows'] = function(row1, row2) {
if (this.getRowValue(row1) < this.getRowValue(row2))
return -1;
else if (this.getRowValue(row1) > this.getRowValue(row2))
return 1;
else return 0;
};
column['updateTd'] = function(td, row) {
const value = this.getRowValue(row)
td.set('text', value);
td.set('title', value);
};
column['onResize'] = null;
this.columns.push(column);
this.columns[name] = column;
this.hiddenTableHeader.appendChild(new Element('th'));
this.fixedTableHeader.appendChild(new Element('th'));
},
setupCommonEvents: function() {
const scrollFn = function() {
$(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
}.bind(this);
$(this.dynamicTableDivId).addEvent('scroll', scrollFn);
}
});
const RssDownloaderRulesTable = new Class({
Extends: DynamicTable,
initColumns: function() {
this.newColumn('checked', '', '', 30, true);
this.newColumn('name', '', '', -1, true);
this.columns['checked'].updateTd = function(td, row) {
if ($('cbRssDlRule' + row.rowId) === null) {
const checkbox = new Element('input');
checkbox.set('type', 'checkbox');
checkbox.set('id', 'cbRssDlRule' + row.rowId);
checkbox.checked = row.full_data.checked;
checkbox.addEvent('click', function(e) {
window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
rowId: row.rowId,
checked: this.checked
});
window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, 'enabled', this.checked);
e.stopPropagation();
});
td.append(checkbox);
}
else {
$('cbRssDlRule' + row.rowId).checked = row.full_data.checked;
}
};
},
setupHeaderMenu: function() {},
setupHeaderEvents: function() {},
getFilteredAndSortedRows: function() {
return this.rows.getValues();
},
setupTr: function(tr) {
tr.addEvent('dblclick', function(e) {
window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
return true;
});
},
newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
const column = {};
column['name'] = name;
column['title'] = name;
column['visible'] = defaultVisible;
column['force_hide'] = false;
column['caption'] = caption;
column['style'] = style;
if (defaultWidth !== -1) {
column['width'] = defaultWidth;
}
column['dataProperties'] = [name];
column['getRowValue'] = function(row, pos) {
if (pos === undefined)
pos = 0;
return row['full_data'][this.dataProperties[pos]];
};
column['compareRows'] = function(row1, row2) {
if (this.getRowValue(row1) < this.getRowValue(row2))
return -1;
else if (this.getRowValue(row1) > this.getRowValue(row2))
return 1;
else return 0;
};
column['updateTd'] = function(td, row) {
const value = this.getRowValue(row)
td.set('text', value);
td.set('title', value);
};
column['onResize'] = null;
this.columns.push(column);
this.columns[name] = column;
this.hiddenTableHeader.appendChild(new Element('th'));
this.fixedTableHeader.appendChild(new Element('th'));
},
selectRow: function(rowId) {
this.selectedRows.push(rowId);
this.setRowClass();
this.onSelectedRowChanged();
const rows = this.rows.getValues();
let name = '';
for (let i = 0; i < rows.length; ++i) {
if (rows[i].rowId === rowId) {
name = rows[i].full_data.name;
break;
}
}
window.qBittorrent.RssDownloader.showRule(name);
}
});
const RssDownloaderFeedSelectionTable = new Class({
Extends: DynamicTable,
initColumns: function() {
this.newColumn('checked', '', '', 30, true);
this.newColumn('name', '', '', -1, true);
this.columns['checked'].updateTd = function(td, row) {
if ($('cbRssDlFeed' + row.rowId) === null) {
const checkbox = new Element('input');
checkbox.set('type', 'checkbox');
checkbox.set('id', 'cbRssDlFeed' + row.rowId);
checkbox.checked = row.full_data.checked;
checkbox.addEvent('click', function(e) {
window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
rowId: row.rowId,
checked: this.checked
});
e.stopPropagation();
});
td.append(checkbox);
}
else {
$('cbRssDlFeed' + row.rowId).checked = row.full_data.checked;
}
};
},
setupHeaderMenu: function() {},
setupHeaderEvents: function() {},
getFilteredAndSortedRows: function() {
return this.rows.getValues();
},
newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
const column = {};
column['name'] = name;
column['title'] = name;
column['visible'] = defaultVisible;
column['force_hide'] = false;
column['caption'] = caption;
column['style'] = style;
if (defaultWidth !== -1) {
column['width'] = defaultWidth;
}
column['dataProperties'] = [name];
column['getRowValue'] = function(row, pos) {
if (pos === undefined)
pos = 0;
return row['full_data'][this.dataProperties[pos]];
};
column['compareRows'] = function(row1, row2) {
if (this.getRowValue(row1) < this.getRowValue(row2))
return -1;
else if (this.getRowValue(row1) > this.getRowValue(row2))
return 1;
else return 0;
};
column['updateTd'] = function(td, row) {
const value = this.getRowValue(row)
td.set('text', value);
td.set('title', value);
};
column['onResize'] = null;
this.columns.push(column);
this.columns[name] = column;
this.hiddenTableHeader.appendChild(new Element('th'));
this.fixedTableHeader.appendChild(new Element('th'));
},
selectRow: function() {}
});
const RssDownloaderArticlesTable = new Class({
Extends: DynamicTable,
initColumns: function() {
this.newColumn('name', '', '', -1, true);
},
setupHeaderMenu: function() {},
setupHeaderEvents: function() {},
getFilteredAndSortedRows: function() {
return this.rows.getValues();
},
newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
const column = {};
column['name'] = name;
column['title'] = name;
column['visible'] = defaultVisible;
column['force_hide'] = false;
column['caption'] = caption;
column['style'] = style;
if (defaultWidth !== -1) {
column['width'] = defaultWidth;
}
column['dataProperties'] = [name];
column['getRowValue'] = function(row, pos) {
if (pos === undefined)
pos = 0;
return row['full_data'][this.dataProperties[pos]];
};
column['compareRows'] = function(row1, row2) {
if (this.getRowValue(row1) < this.getRowValue(row2))
return -1;
else if (this.getRowValue(row1) > this.getRowValue(row2))
return 1;
else return 0;
};
column['updateTd'] = function(td, row) {
const value = this.getRowValue(row)
td.set('text', value);
td.set('title', value);
};
column['onResize'] = null;
this.columns.push(column);
this.columns[name] = column;
this.hiddenTableHeader.appendChild(new Element('th'));
this.fixedTableHeader.appendChild(new Element('th'));
},
selectRow: function() {},
updateRow: function(tr, fullUpdate) {
const row = this.rows.get(tr.rowId);
const data = row[fullUpdate ? 'full_data' : 'data'];
if (row.full_data.isFeed) {
tr.addClass('articleTableFeed');
tr.removeClass('articleTableArticle');
}
else {
tr.removeClass('articleTableFeed');
tr.addClass('articleTableArticle');
}
const tds = tr.getElements('td');
for (let i = 0; i < this.columns.length; ++i) {
if (data.hasOwnProperty(this.columns[i].dataProperties[0]))
this.columns[i].updateTd(tds[i], row);
}
row['data'] = {};
}
});
return exports(); return exports();
})(); })();

64
src/webui/www/private/views/preferences.html

@ -595,6 +595,54 @@
</fieldset> </fieldset>
</div> </div>
<div id="RSSTab" class="PrefTab invisible">
<fieldset class="settings">
<legend>QBT_TR(RSS Reader)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="enable_fetching_rss_feeds_checkbox" />
<label for="enable_fetching_rss_feeds_checkbox">QBT_TR(Enable fetching RSS feeds)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<table>
<tr>
<td>
<label for="feed_refresh_interval">QBT_TR(Feeds refresh interval:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="feed_refresh_interval" style="width: 4em;" />&nbsp;&nbsp;QBT_TR( min)QBT_TR[CONTEXT=OptionsDialog]
</td>
</tr>
<tr>
<td>
<label for="maximum_article_number">QBT_TR(Maximum number of articles per feed:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="maximum_article_number" style="width: 4em;" />
</td>
</tr>
</table>
</fieldset>
<fieldset class="settings">
<legend>QBT_TR(RSS Torrent Auto Downloader)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="enable_auto_downloading_rss_torrents_checkbox" />
<label for="enable_auto_downloading_rss_torrents_checkbox">QBT_TR(Enable auto downloading of RSS torrents)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<button style="margin: 0 1em; width: calc(100% - 2.2em)" onclick="window.qBittorrent.Rss.openRssDownloader();">QBT_TR(Edit auto downloading rules...)QBT_TR[CONTEXT=OptionsDialog]</button>
</fieldset>
<fieldset class="settings">
<legend>QBT_TR(RSS Smart Episode Filter)QBT_TR[CONTEXT=OptionsDialog]</legend>
<div class="formRow">
<input type="checkbox" id="downlock_repack_proper_episodes" />
<label for="downlock_repack_proper_episodes">QBT_TR(Download REPACK/PROPER episodes)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
<label for="rss_filter_textarea">QBT_TR(Filters:)QBT_TR[CONTEXT=OptionsDialog]</label><br>
<textarea id="rss_filter_textarea" rows="6" cols="70"></textarea>
</fieldset>
</div>
<div id="WebUITab" class="PrefTab invisible"> <div id="WebUITab" class="PrefTab invisible">
<fieldset class="settings"> <fieldset class="settings">
<legend>QBT_TR(Language)QBT_TR[CONTEXT=OptionsDialog]</legend> <legend>QBT_TR(Language)QBT_TR[CONTEXT=OptionsDialog]</legend>
@ -1723,6 +1771,14 @@
$('add_trackers_textarea').setProperty('value', pref.add_trackers); $('add_trackers_textarea').setProperty('value', pref.add_trackers);
updateAddTrackersEnabled(); updateAddTrackersEnabled();
// RSS Tab
$('enable_fetching_rss_feeds_checkbox').setProperty('checked', pref.rss_processing_enabled);
$('feed_refresh_interval').setProperty('value', pref.rss_refresh_interval);
$('maximum_article_number').setProperty('value', pref.rss_max_articles_per_feed);
$('enable_auto_downloading_rss_torrents_checkbox').setProperty('checked', pref.rss_auto_downloading_enabled);
$('downlock_repack_proper_episodes').setProperty('checked', pref.rss_download_repack_proper_episodes);
$('rss_filter_textarea').setProperty('value', pref.rss_smart_episode_filters);
// Web UI tab // Web UI tab
// Language // Language
$('locale_select').setProperty('value', pref.locale); $('locale_select').setProperty('value', pref.locale);
@ -2077,6 +2133,14 @@
settings.set('add_trackers_enabled', $('add_trackers_checkbox').getProperty('checked')); settings.set('add_trackers_enabled', $('add_trackers_checkbox').getProperty('checked'));
settings.set('add_trackers', $('add_trackers_textarea').getProperty('value')); settings.set('add_trackers', $('add_trackers_textarea').getProperty('value'));
// RSS Tab
settings.set('rss_processing_enabled', $('enable_fetching_rss_feeds_checkbox').getProperty('checked'));
settings.set('rss_refresh_interval', $('feed_refresh_interval').getProperty('value'));
settings.set('rss_max_articles_per_feed', $('maximum_article_number').getProperty('value'));
settings.set('rss_auto_downloading_enabled', $('enable_auto_downloading_rss_torrents_checkbox').getProperty('checked'));
settings.set('rss_download_repack_proper_episodes', $('downlock_repack_proper_episodes').getProperty('checked'));
settings.set('rss_smart_episode_filters', $('rss_filter_textarea').getProperty('value'));
// Web UI tab // Web UI tab
// Language // Language
settings.set('locale', $('locale_select').getProperty('value')); settings.set('locale', $('locale_select').getProperty('value'));

5
src/webui/www/private/views/preferencesToolbar.html

@ -5,6 +5,7 @@
<li id="PrefConnectionLink"><a>QBT_TR(Connection)QBT_TR[CONTEXT=OptionsDialog]</a></li> <li id="PrefConnectionLink"><a>QBT_TR(Connection)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefSpeedLink"><a>QBT_TR(Speed)QBT_TR[CONTEXT=OptionsDialog]</a></li> <li id="PrefSpeedLink"><a>QBT_TR(Speed)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefBittorrentLink"><a>QBT_TR(BitTorrent)QBT_TR[CONTEXT=OptionsDialog]</a></li> <li id="PrefBittorrentLink"><a>QBT_TR(BitTorrent)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefRSSLink"><a>QBT_TR(RSS)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefWebUILink"><a>QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</a></li> <li id="PrefWebUILink"><a>QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</a></li>
<li id="PrefAdvancedLink"><a>QBT_TR(Advanced)QBT_TR[CONTEXT=OptionsDialog]</a></li> <li id="PrefAdvancedLink"><a>QBT_TR(Advanced)QBT_TR[CONTEXT=OptionsDialog]</a></li>
</ul> </ul>
@ -34,6 +35,10 @@
$$('.PrefTab').addClass('invisible'); $$('.PrefTab').addClass('invisible');
$('BittorrentTab').removeClass('invisible'); $('BittorrentTab').removeClass('invisible');
}); });
$('PrefRSSLink').addEvent('click', function(e) {
$$('.PrefTab').addClass('invisible');
$('RSSTab').removeClass('invisible');
});
$('PrefWebUILink').addEvent('click', function(e) { $('PrefWebUILink').addEvent('click', function(e) {
$$('.PrefTab').addClass('invisible'); $$('.PrefTab').addClass('invisible');
$('WebUITab').removeClass('invisible'); $('WebUITab').removeClass('invisible');

823
src/webui/www/private/views/rss.html

@ -0,0 +1,823 @@
<style>
#rssView {
padding: 20px 20px 0 20px;
height: calc(100% - 20px);
}
#rssContentView {
display: table;
width: 100%;
height: calc(100% - 30px);
vertical-align: top;
}
#rssFeedFixedHeaderDiv .dynamicTableHeader, #rssArticleFixedHeaderDiv .dynamicTableHeader {
cursor: default;
}
.alignRight {
float: right;
}
.unreadArticle {
color: blue;
}
#rssFetchingDisabled {
color: red;
font-style: italic;
margin-bottom: 10px;
}
#centerRssColumn {
margin: 0 10px 0 10px;
}
#leftRssColumn, #centerRssColumn, #rightRssColumn {
float: left;
/* should be 20 px but due to rounding differences some browsers don't render that properly */
width: calc(calc(100% - 21px) / 3);
border: none;
}
#rightRssColumn {
overflow: auto;
}
#rssFeedTableDiv, #rssArticleTableDiv {
height: calc(100vh - 180px);
}
#rssTorrentDetailsName {
background-color: #678db2;
padding: 0;
color: white;
}
#rssTorrentDetailsDate {
background-color: #EFEFEF;
}
#rssDetailsView {
height: calc(100vh - 135px);
overflow: auto;
}
#rssButtonBar {
overflow: hidden;
height: 30px;
}
#rssContentView table {
width: 100%;
}
</style>
<div id="rssView">
<div id="rssTopBar">
<div id="rssFetchingDisabled" class="invisible">
QBT_TR(Fetching of RSS feeds is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=RSSWidget]
</div>
<div id="rssButtonBar">
<button id="newSubscriptionButton" onclick="qBittorrent.Rss.addRSSFeed()">QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="markReadButton" onclick="qBittorrent.Rss.markSelectedAsRead()">QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]</button>
<button id="rssDownloaderButton" class="alignRight" onclick="qBittorrent.Rss.openRssDownloader()">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]</button>
</div>
</div>
<div id="rssContentView">
<div id="leftRssColumn">
<div id="rssFeedFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssFeedTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="centerRssColumn">
<div id="rssArticleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssArticleTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="rightRssColumn">
<div id="rssDetailsView"></div>
</div>
</div>
</div>
<ul id="rssFeedMenu" class="contextMenu">
<li><a href="#update"><img src="icons/view-refresh.svg" alt="QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#markRead"><img src="icons/mail-mark-read.svg" alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#rename"><img src="icons/edit-rename.svg" alt="QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#delete"><img src="icons/edit-delete.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#newSubscription"><img src="icons/document-new.svg" alt="QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#newFolder"><img src="icons/folder-new.svg" alt="QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#updateAll"><img src="icons/view-refresh.svg" alt="QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li class="separator"><a href="#copyFeedURL" id="CopyFeedURL"><img src="icons/edit-copy.svg" alt="QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<ul id="rssArticleMenu" class="contextMenu">
<li><a href="#Download"><img src="icons/download.svg" alt="QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]</a></li>
<li><a href="#OpenNews"><img src="icons/application-x-mswinurl.svg" alt="QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]" /> QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>
<script>
'use strict';
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
let serverSyncRssDataInterval = 1500;
window.qBittorrent.Rss = (() => {
const exports = () => {
return {
init: init,
unload: unload,
load: load,
showRssFeed: showRssFeed,
showDetails: showDetails,
updateRssFeedList: updateRssFeedList,
refreshAllFeeds: refreshAllFeeds,
moveItem: moveItem,
addRSSFeed: addRSSFeed,
markSelectedAsRead : markSelectedAsRead,
openRssDownloader: openRssDownloader,
rssFeedTable: rssFeedTable
};
};
let feedData = {};
let pathByFeedId = new Map();;
let feedRefreshTimer;
let rssFeedTable = new window.qBittorrent.DynamicTable.RssFeedTable();
let rssArticleTable = new window.qBittorrent.DynamicTable.RssArticleTable();
const init = () => {
new Request.JSON({
url: 'api/v2/app/preferences',
method: 'get',
noCache: true,
onFailure: () => {
alert('Could not contact qBittorrent');
},
onSuccess: (pref) => {
if (!pref.rss_processing_enabled)
$('rssFetchingDisabled').removeClass('invisible');
// recalculate heights
let nonPageHeight = $('rssTopBar').getBoundingClientRect().height +
$('desktopHeader').getBoundingClientRect().height +
$('desktopFooterWrapper').getBoundingClientRect().height + 20;
$('rssDetailsView').style.height = 'calc(100vh - ' + nonPageHeight + 'px)';
let nonTableHeight = nonPageHeight + $('rssFeedFixedHeaderDiv').getBoundingClientRect().height;
$('rssFeedTableDiv').style.height = 'calc(100vh - ' + nonTableHeight + 'px)';
$('rssArticleTableDiv').style.height = 'calc(100vh - ' + nonTableHeight + 'px)';
$('rssContentView').style.height = 'calc(100% - ' + $('rssTopBar').getBoundingClientRect().height + 'px)';
}
}).send();
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
targets: '.rssFeedContextMenuTarget',
menu: 'rssFeedMenu',
actions: {
update: (el) => {
let feedsToUpdate = new Set();
rssFeedTable.selectedRows.each((rowId) => {
let selectedPath = rssFeedTable.rows[rowId].full_data.dataPath;
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, selectedPath.length) === selectedPath)
.filter((row) => row.full_data.dataUid !== '')
.each((row) => feedsToUpdate.add(row));
});
feedsToUpdate.forEach((feed) => refreshFeed(feed.full_data.dataUid))
},
markRead: markSelectedAsRead,
rename: (el) => {
moveItem(rssFeedTable.rows[rssFeedTable.selectedRows[0]].full_data.dataPath);
},
delete: (el) => {
let selectedDatapaths = rssFeedTable.selectedRows
.filter((e) => e !== 0)
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
let reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
removeItem(reducedDatapaths);
},
newSubscription: addRSSFeed,
newFolder: addFolder,
updateAll: refreshAllFeeds
},
offsets: {
x: -16,
y: -57
}
});
rssFeedContextMenu.addTarget($('rssFeedTableDiv'));
// deselect feed when clicking on empty part of table
$('rssFeedTableDiv').addEventListener('click', (e) => {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
});
$('rssFeedTableDiv').addEventListener('contextmenu', (e) => {
if (e.toElement.nodeName === 'DIV') {
rssFeedTable.deselectAll();
rssFeedTable.deselectRow();
rssFeedContextMenu.updateMenuItems();
}
});
new ClipboardJS('#CopyFeedURL', {
text: () => {
let joined = '';
rssFeedTable.selectedRows
.filter((row) => rssFeedTable.rows[row].full_data.dataUid !== '')
.each((row) => joined += rssFeedTable.rows[row].full_data.dataUrl + '\n');
return joined.slice(0, -1);
}
});
rssFeedTable.setup('rssFeedTableDiv', 'rssFeedFixedHeaderDiv', rssFeedContextMenu);
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
targets: '.rssArticleElement',
menu: 'rssArticleMenu',
actions: {
Download: (el) => {
let dlString = '';
rssArticleTable.selectedRows.each((row) => {
dlString += rssArticleTable.rows[row].full_data.torrentURL + '\n';
});
showDownloadPage([dlString]);
},
OpenNews: (el) => {
rssArticleTable.selectedRows.each((row) => {
window.open(rssArticleTable.rows[row].full_data.link);
});
}
},
offsets: {
x: -16,
y: -57
}
});
rssArticleTable.setup('rssArticleTableDiv', 'rssArticleFixedHeaderDiv', rssArticleContextMenu);
updateRssFeedList();
load();
};
const unload = () => {
clearInterval(feedRefreshTimer);
}
const load = () => {
feedRefreshTimer = setInterval(updateRssFeedList, serverSyncRssDataInterval);
}
const addRSSFeed = () => {
let path = '';
if (rssFeedTable.selectedRows.length !== 0) {
let row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === '') {
path = row.full_data.dataPath;
}
else {
let lastIndex = row.full_data.dataPath.lastIndexOf('\\');
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: 'newFeed',
title: 'QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'newfeed.html?path=' + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const addFolder = () => {
let path = '';
if (rssFeedTable.selectedRows.length !== 0) {
let row = rssFeedTable.rows[rssFeedTable.selectedRows[0]];
if (row.full_data.dataUid === '') {
path = row.full_data.dataPath;
}
else {
let lastIndex = row.full_data.dataPath.lastIndexOf('\\');
if (lastIndex !== -1)
path = row.full_data.dataPath.slice(0, lastIndex);
}
}
new MochaUI.Window({
id: 'newFolder',
title: 'QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'newfolder.html?path=' + encodeURIComponent(path),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
}
const showRssFeed = (path) => {
rssArticleTable.clear();
let rowCount = 0;
let childFeeds = new Set();
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, path.length) === path)
.filter((row) => row.full_data.dataUid !== '')
.each((row) => childFeeds.add(row.full_data.dataUid));
let visibleArticles = [];
for (let feedEntry in feedData) {
if (childFeeds.has(feedEntry))
visibleArticles.append(feedData[feedEntry]
.map((a) => {
a.feedUid = feedEntry;
return a;
}));
}
//filter read articles if "Unread" feed is selected
if (path === '')
visibleArticles = visibleArticles.filter((a) => !a.isRead);
visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
.each((torrentEntry) => {
rssArticleTable.updateRowData({
rowId: rowCount++,
name: torrentEntry.title,
link: torrentEntry.link,
torrentURL: torrentEntry.torrentURL,
feedUid: torrentEntry.feedUid,
dataId: torrentEntry.id,
isRead: torrentEntry.isRead
});
});
$('rssDetailsView').empty();
rssArticleTable.updateTable(false);
};
const showDetails = (feedUid, articleID) => {
markArticleAsRead(pathByFeedId.get(feedUid), articleID);
$('rssDetailsView').empty();
let article = feedData[feedUid].filter((article) => article.id === articleID)[0];
if (article) {
$('rssDetailsView').append((() => {
let torrentName = document.createElement('p');
torrentName.innerText = article.title;
torrentName.setAttribute('id', 'rssTorrentDetailsName');
return torrentName;
})());
$('rssDetailsView').append((() => {
let torrentDate = document.createElement('div');
torrentDate.setAttribute('id', 'rssTorrentDetailsDate');
let torrentDateDesc = document.createElement('b');
torrentDateDesc.innerText = 'QBT_TR(Date: )QBT_TR[CONTEXT=RSSWidget]';
torrentDate.append(torrentDateDesc);
let torrentDateData = document.createElement('span');
torrentDateData.innerText = new Date(article.date).toLocaleString();
torrentDate.append(torrentDateData);
return torrentDate;
})());
// Strip script before interpreting html
let torrentDescription = document.createRange().createContextualFragment(
'<div id="rssTorrentDetailsDescription">' + article.description.stripScripts() + '</div>');
$('rssDetailsView').append(torrentDescription);
}
};
const updateRssFeedList = () => {
new Request.JSON({
url: 'api/v2/rss/items',
noCache: true,
method: 'post',
data: {
withData: true
},
onSuccess: (response) => {
// flatten folder structure
let flattenedResp = [];
let recFlatten = (current, name = '', depth = 0, fullName = '') => {
for (let child in current) {
let currentFullName = fullName ? (fullName + '\\' + child) : child;
if (current[child].uid !== undefined) {
current[child].name = child;
current[child].isFolder = false;
current[child].depth = depth;
current[child].fullName = currentFullName;
flattenedResp.push(current[child]);
}
else {
flattenedResp.push({
name: child,
isFolder: true,
depth: depth,
fullName: currentFullName
});
recFlatten(current[child], child, depth + 1, currentFullName);
}
}
}
recFlatten(response);
// check if rows matche flattend response
let match = false;
if (rssFeedTable.rows.getLength() - 1 === flattenedResp.length) {
match = true;
for (let i = 0; i < flattenedResp.length; ++i) {
if ((flattenedResp[i].uid ? flattenedResp[i].uid : '') !== rssFeedTable.rows[i + 1].full_data.dataUid ||
flattenedResp[i].fullName !== rssFeedTable.rows[i + 1].full_data.dataPath) {
match = false;
break;
}
}
}
if (match) {
// partial refresh
// update status
let statusDiffers = false;
for (let i = 0; i < flattenedResp.length; ++i) {
let oldStatus = rssFeedTable.rows[i + 1].full_data.status;
let status = 'default';
if (flattenedResp[i].hasError)
status = 'hasError';
if (flattenedResp[i].isLoading)
status = 'isLoading';
if (flattenedResp[i].isFolder)
status = 'isFolder';
if (oldStatus !== status) {
statusDiffers = true;
rssFeedTable.updateRowData({
rowId: i + 1,
status: status
});
}
}
if (statusDiffers)
rssFeedTable.updateIcons();
// get currently opened feed
let openedFeedPath = undefined;
if (rssFeedTable.selectedRows.length !== 0) {
let lastSelectedRow = rssFeedTable.selectedRows[rssFeedTable.selectedRows.length - 1];
openedFeedPath = rssFeedTable.rows[lastSelectedRow].full_data.dataPath;
}
// check if list of articles differs
let needsUpdate = false;
flattenedResp.filter((r) => !r.isFolder)
.each((r) => {
let articlesDiffer = true;
if (r.articles.length === feedData[r.uid].length) {
articlesDiffer = false;
for (let i = 0; i < r.articles.length; ++i) {
if (feedData[r.uid][i].id !== r.articles[i].id)
articlesDiffer = true;
}
}
if (articlesDiffer) {
// update unread count
let oldUnread = feedData[r.uid].map((art) => !art.isRead).filter((v) => v).length;
let newUnread = r.articles.map((art) => !art.isRead).filter((v) => v).length;
let unreadDifference = newUnread - oldUnread;
// find all parents (and self) and add unread difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += unreadDifference);
needsUpdate = true;
// update data
feedData[r.uid] = r.articles;
// if feed that is open changed, reload
if (openedFeedPath !== undefined && r.fullName.slice(0, openedFeedPath.length) === openedFeedPath)
showRssFeed(r.fullName);
}
else {
// calculate read differnce and update feed data
let readDifference = 0;
let readChanged = false;
for (let i = 0; i < r.articles.length; ++i) {
let oldRead = feedData[r.uid][i].isRead ? 1 : 0;
let newRead = r.articles[i].isRead ? 1 : 0;
feedData[r.uid][i].isRead = r.articles[i].isRead;
readDifference += oldRead - newRead;
if (readDifference !== 0)
readChanged = true;
}
// if read on article changed
if (readChanged) {
needsUpdate = true;
// find all items that contain this rss feed and add read difference
rssFeedTable.rows.filter((row) => r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += readDifference);
// if feed that is opened changed update dynamicly
if (openedFeedPath !== undefined && r.fullName.slice(0, openedFeedPath.length) === openedFeedPath) {
for (let i = 0; i < r.articles.length; ++i) {
let matchingRow = rssArticleTable.rows.filter((row) => row.full_data.feedUid === r.uid)
.filter((row) => row.full_data.dataId === r.articles[i].id);
matchingRow[Object.keys(matchingRow)[0]].full_data.isRead = r.articles[i].isRead;
}
}
}
}
});
if (needsUpdate) {
rssFeedTable.updateTable(true);
rssArticleTable.updateTable(true);
}
}
else {
// full refresh
rssFeedTable.clear();
feedData = {};
pathByFeedId = new Map();
// Unread entry at top
rssFeedTable.updateRowData({
rowId: 0,
name: 'Unread',
unread: 0,
status: 'unread',
indentaion: 0,
dataUid: '',
dataUrl: '',
dataPath: ''
});
let rowCount = 1;
for (let dataEntry of flattenedResp) {
if (dataEntry.isFolder) {
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: 'isFolder',
indentaion: dataEntry.depth,
dataUid: '',
dataUrl: '',
dataPath: dataEntry.fullName
});
}
else {
let status = 'default';
if (dataEntry.hasError)
status = 'hasError';
if (dataEntry.isLoading)
status = 'isLoading';
rssFeedTable.updateRowData({
rowId: rowCount,
name: dataEntry.name,
unread: 0,
status: status,
indentaion: dataEntry.depth,
dataUid: dataEntry.uid,
dataUrl: dataEntry.url,
dataPath: dataEntry.fullName
});
// calculate number of unread
let numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter((v) => v).length;
// find all items that contain this rss feed and add unread count
rssFeedTable.rows.filter((row) => dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread += numberOfUnread);
pathByFeedId.set(dataEntry.uid, dataEntry.fullName);
feedData[dataEntry.uid] = dataEntry.articles;
}
++rowCount;
}
rssFeedTable.updateTable(false);
rssFeedTable.updateIcons();
}
}
}).send();
};
const refreshFeed = (feedUid) => {
// set icon to loading
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataUid === feedUid)
row.full_data.status = 'isLoading';
});
rssFeedTable.updateIcons();
new Request({
url: 'api/v2/rss/refreshItem',
noCache: true,
method: 'post',
data: {
itemPath: pathByFeedId.get(feedUid)
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const refreshAllFeeds = () => {
for (let feedEntry in feedData)
refreshFeed(feedEntry);
};
const moveItem = (oldPath) => {
new MochaUI.Window({
id: 'renamePage',
title: 'QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'rename_feed.html?oldPath=' + encodeURIComponent(oldPath),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeItem = (paths) => {
new MochaUI.Window({
id: 'confirmFeedDeletionPage',
title: 'QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]',
loadMethod: 'iframe',
contentURL: 'confirmfeeddeletion.html?paths=' + encodeURIComponent(paths.join('|')),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 70
});
};
const markItemAsRead = (path) => {
// feed data mark as read
for (let feedID in feedData)
if (pathByFeedId.get(feedID).slice(0, path.length) === path)
feedData[feedID].each((el) => el.isRead = true);
// mark rows as read
rssArticleTable.rows.each((el) => el.full_data.isRead = true);
// find all children and set unread count to 0
rssFeedTable.rows.filter((row) => row.full_data.dataPath.slice(0, path.length) === path && path !== row.full_data.dataPath)
.each((row) => row.full_data.unread = 0);
// find selected row
let rowId, prevUnreadCount;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
prevUnreadCount = row.full_data.unread;
}
});
// find all parents (and self) and subtract previous unread count
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= prevUnreadCount);
rssArticleTable.updateTable(false);
rssFeedTable.updateTable(true);
// send request
new Request({
url: 'api/v2/rss/markAsRead',
noCache: true,
method: 'post',
data: {
itemPath: path
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
};
const markArticleAsRead = (path, id) => {
// find row
let rowId, name, uid, unread;
rssFeedTable.rows.forEach((row) => {
if (row.full_data.dataPath === path) {
rowId = row.full_data.rowId;
name = row.full_data.dataPath;
uid = row.full_data.dataUid;
unread = row.full_data.unread;
}
});
// update feed data
let prevReadState = true;
feedData[uid].each((article) => {
if (article.id === id) {
prevReadState = article.isRead;
article.isRead = true;
}
});
if (!prevReadState) {
// find all items that contain this feed and subtract 1
rssFeedTable.rows.filter((row) => path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
.each((row) => row.full_data.unread -= 1);
rssFeedTable.updateTable(true);
new Request({
url: 'api/v2/rss/markAsRead',
noCache: true,
method: 'post',
data: {
itemPath: path,
articleId: id
},
onFailure: (response) => {
if (response.status === 409)
alert(response.responseText);
}
}).send();
}
};
const markSelectedAsRead = () => {
let selectedDatapaths = rssFeedTable.selectedRows
.map((sRow) => rssFeedTable.rows[sRow].full_data.dataPath);
// filter children
let reducedDatapaths = selectedDatapaths.filter((path) =>
selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
);
reducedDatapaths.each((path) => markItemAsRead(path));
};
const openRssDownloader = () => {
const id = 'rssdownloaderpage';
new MochaUI.Window({
id: id,
title: 'QBT_TR(Rss Downloader)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'xhr',
contentURL: 'views/rssDownloader.html',
maximizable: false,
width: loadWindowWidth(id, 800),
height: loadWindowHeight(id, 650),
onResize: () => {
saveWindowSize(id);
},
resizeLimit: {
'x' :[800,2500],
'y' :[500,2000]
}
});
};
return exports();
})();
</script>

735
src/webui/www/private/views/rssDownloader.html

@ -0,0 +1,735 @@
<style>
#rssdownloaderpage_content {
height: calc(100% - 30px);
}
#RssDownloader {
height: 100%;
}
#leftRssDownloaderColumn {
width: 25%;
float: left;
}
#rulesTable, #rssDownloaderArticlesTable {
overflow: auto;
width: 100%;
height: 100%;
}
#centerRssDownloaderColumn {
width: 50%;
float: left;
}
#rightRssDownloaderColumn {
width: 25%;
float: left;
}
.fullWidth {
width: 100%;
max-width: none;
}
.noWrap {
white-space: nowrap;
}
#rssDownloaderFeeds {
height: calc(100% - 355px);
overflow: hidden;
}
#rssDownloaderFeedsTable {
height: calc(100% - 21px);
width: 100%;
overflow: auto;
}
#saveButton {
margin-top: 5px;
width: 100%;
}
.articleTableFeed td {
font-weight: bold;
}
.articleTableArticle td {
padding-left: 22px;
}
#rssDownloaderDisabled {
color: red;
font-style: italic;
margin-bottom: 10px;
}
#rulesTable table, #rssDownloaderFeedsTable table, #rssDownloaderArticlesTable table {
width: 100%;
}
#ignoreDaysValue {
width: 4em;
}
#lastMatchDiv {
float: right;
}
</style>
<div id="RssDownloader" class="RssDownloader">
<div id="rssDownloaderDisabled" class="invisible">
QBT_TR(Auto downloading of RSS torrents is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=AutomatedRssDownloader]
</div>
<div id="leftRssDownloaderColumn">
<b id="rulesTableDesc">QBT_TR(Download Rules)QBT_TR[CONTEXT=AutomatedRssDownloader]</b>
<div id="rulesTable">
<div id="rulesSelectionCheckBoxList">
<div id="rssDownloaderRuleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderRuleTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="centerRssDownloaderColumn">
<fieldset class="settings" id="ruleSettings">
<legend>QBT_TR(Rule Definition)QBT_TR[CONTEXT=AutomatedRssDownloader]</legend>
<div class="formRow">
<input disabled type="checkbox" id="useRegEx" onclick="qBittorrent.RssDownloader.setElementTitles()"/>
<label for="useRegEx">QBT_TR(Use Regular Expressions)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<table class="fullWidth">
<tr>
<td>
<label for="mustContainText" class="noWrap">QBT_TR(Must Contain:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="mustContainText" class="fullWidth" autocorrect="off" autocapitalize="none" />
</td>
</tr>
<tr>
<td>
<label for="mustNotContainText" class="noWrap">QBT_TR(Must Not Contain:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="mustNotContainText" class="fullWidth" autocorrect="off" autocapitalize="none" />
</td>
</tr>
<tr>
<td>
<label for="episodeFilterText" class="noWrap">QBT_TR(Episode Filter:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<input disabled type="text" id="episodeFilterText" class="fullWidth" autocorrect="off" autocapitalize="none" />
</td>
</tr>
</table>
<div class="formRow" title="QBT_TR(Smart Episode Filter will check the episode number to prevent downloading of duplicates.
Supports the formats: S01E01, 1x1, 2017.01.01 and 01.01.2017 (Date formats also support - as a separator))QBT_TR[CONTEXT=AutomatedRssDownloader]">
<input disabled type="checkbox" id="useSmartFilter" />
<label for="useSmartFilter">QBT_TR(Use Smart Episode Filter)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<hr>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Assign Category:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<select disabled id="assignCategoryCombobox" class="fullWidth">
<option value=""></option>
</select>
</td>
</tr>
</table>
<div class="formRow">
<input disabled type="checkbox" id="savetoDifferentDir" />
<label for="savetoDifferentDir">QBT_TR(Save to a Different Directory)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</div>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap" for="saveToText">QBT_TR(Save to:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<input disabled type="text" class="fullWidth" id="saveToText" autocorrect="off" autocapitalize="none" />
</td>
</tr>
</table>
<table>
<tr>
<td><label for="ignoreDaysValue">QBT_TR(Ignore Subsequent Matches for (0 to Disable))QBT_TR[CONTEXT=AutomatedRssDownloader]</label></td>
<td><input type="number" id="ignoreDaysValue" min="0" />QBT_TR( days)QBT_TR[CONTEXT=AutomatedRssDownloader]</td>
</tr>
</table>
<div id="lastMatchDiv">
<span id="lastMatchText">QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]</span>
</div>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Add Paused:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<select disabled id="addPausedCombobox" class="fullWidth">
<option value="default">QBT_TR(Use global settings)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="always">QBT_TR(Always)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="never">QBT_TR(Never)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
</select>
</td>
</tr>
</table>
<table class="fullWidth">
<tr>
<td>
<label class="noWrap">QBT_TR(Create Subfolder:)QBT_TR[CONTEXT=AutomatedRssDownloader]</label>
</td>
<td class="fullWidth">
<select disabled id="creatSubfolderCombobox" class="fullWidth">
<option value="default">QBT_TR(Use global settings)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="always">QBT_TR(Always)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
<option value="never">QBT_TR(Never)QBT_TR[CONTEXT=AutomatedRssDownloader]</option>
</select>
</td>
</tr>
</table>
</fieldset>
<fieldset class="settings" id="rssDownloaderFeeds">
<legend>QBT_TR(Apply Rule to Feeds:)QBT_TR[CONTEXT=AutomatedRssDownloader]</legend>
<div id="rssDownloaderFeedsTable">
<div id="rssDownloaderFeedSelectionFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderFeedSelectionTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</fieldset>
<button disabled id="saveButton" onclick="qBittorrent.RssDownloader.saveSettings()">
QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]
</button>
</div>
<div id="rightRssDownloaderColumn">
<b id="articleTableDesc">QBT_TR(Matching RSS Articles)QBT_TR[CONTEXT=AutomatedRssDownloader]</b>
<div id="rssDownloaderArticlesTable">
<div id="rssDownloaderArticlesFixedHeaderDiv" class="dynamicTableFixedHeaderDiv invisible">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
</table>
</div>
<div id="rssDownloaderArticlesTableDiv" class="dynamicTableDiv">
<table class="dynamicTable unselectable">
<thead>
<tr class="dynamicTableHeader"></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<ul id="rssDownloaderRuleMenu" class="contextMenu">
<li><a href="#addRule"><img src="icons/document-new.svg" alt="QBT_TR(Add new rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Add new rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li><a href="#deleteRule"><img src="icons/list-remove.svg" alt="QBT_TR(Delete rule)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Delete rule)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li class="separator"><a href="#renameRule"><img src="icons/edit-rename.svg" alt="QBT_TR(Rename rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Rename rule...)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
<li class="separator"><a href="#clearDownloadedEpisodes"><img src="icons/edit-clear.svg" alt="QBT_TR(Clear downloaded episodes...)QBT_TR[CONTEXT=AutomatedRssDownloader]" /> QBT_TR(Clear downloaded episodes...)QBT_TR[CONTEXT=AutomatedRssDownloader]</a></li>
</ul>
<script>
'use strict';
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
window.qBittorrent.RssDownloader = (() => {
const exports = () => {
return {
updateRulesList: updateRulesList,
showRule: showRule,
renameRule: renameRule,
modifyRuleState: modifyRuleState,
saveSettings: saveSettings,
rssDownloaderRulesTable: rssDownloaderRulesTable,
rssDownloaderFeedSelectionTable: rssDownloaderFeedSelectionTable,
setElementTitles: setElementTitles
};
};
let rssDownloaderRulesTable = new window.qBittorrent.DynamicTable.RssDownloaderRulesTable();
let rssDownloaderFeedSelectionTable = new window.qBittorrent.DynamicTable.RssDownloaderFeedSelectionTable();
let rssDownloaderArticlesTable = new window.qBittorrent.DynamicTable.RssDownloaderArticlesTable();
let rulesList = {};
let feedList = [];
const initRssDownloader = () => {
new Request.JSON({
url: 'api/v2/app/preferences',
method: 'get',
noCache: true,
onFailure: () => {
alert('Could not contact qBittorrent');
},
onSuccess: (pref) => {
if (!pref.rss_auto_downloading_enabled)
$('rssDownloaderDisabled').removeClass('invisible');
// recalculate height
let warningHeight = $('rssDownloaderDisabled').getBoundingClientRect().height;
$('leftRssDownloaderColumn').style.height = 'calc(100% - ' + warningHeight + 'px)';
$('centerRssDownloaderColumn').style.height = 'calc(100% - ' + warningHeight + 'px)';
$('rightRssDownloaderColumn').style.height = 'calc(100% - ' + warningHeight + 'px)';
$('rulesTable').style.height = 'calc(100% - ' + $('rulesTableDesc').getBoundingClientRect().height + 'px)';
$('rssDownloaderArticlesTable').style.height = 'calc(100% - ' + $('articleTableDesc').getBoundingClientRect().height + 'px)';
let centerRowNotTableHeight = $('saveButton').getBoundingClientRect().height +
$('ruleSettings').getBoundingClientRect().height + 15;
$('rssDownloaderFeeds').style.height = 'calc(100% - ' + centerRowNotTableHeight + 'px)';
// firefox calculates the height of the table inside fieldset differently and thus doesn't need the offset
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
$('rssDownloaderFeedsTable').style.height = '100%';
}
else {
let outsideTableHeight = ($('rssDownloaderFeedsTable').getBoundingClientRect().top - $('rssDownloaderFeeds').getBoundingClientRect().top) - 10;
$('rssDownloaderFeedsTable').style.height = 'calc(100% - ' + outsideTableHeight + 'px)';
}
}
}).send();
const rssDownloaderRuleContextMenu = new window.qBittorrent.ContextMenu.RssDownloaderRuleContextMenu({
targets: '',
menu: 'rssDownloaderRuleMenu',
actions: {
addRule: addRule,
deleteRule: (el) => {
removeRule(rssDownloaderRulesTable.selectedRows
.map((sRow) => rssDownloaderRulesTable.rows[sRow].full_data.name));
},
renameRule: (el) => {
renameRule(rssDownloaderRulesTable.rows[rssDownloaderRulesTable.selectedRows[0]].full_data.name);
},
clearDownloadedEpisodes: (el) => {
clearDownloadedEpisodes(rssDownloaderRulesTable.selectedRows
.map((sRow) => rssDownloaderRulesTable.rows[sRow].full_data.name));
}
},
offsets: {
x: -22,
y: -4
}
});
rssDownloaderRulesTable.setup('rssDownloaderRuleTableDiv', 'rssDownloaderRuleFixedHeaderDiv', rssDownloaderRuleContextMenu);
rssDownloaderFeedSelectionTable.setup('rssDownloaderFeedSelectionTableDiv', 'rssDownloaderFeedSelectionFixedHeaderDiv');
rssDownloaderArticlesTable.setup('rssDownloaderArticlesTableDiv', 'rssDownloaderArticlesFixedHeaderDiv');
rssDownloaderRuleContextMenu.addTarget($('rulesTable'));
// deselect feed when clicking on empty part of table
$('rulesTable').addEventListener('click', (e) => {
rssDownloaderRulesTable.deselectAll();
rssDownloaderRulesTable.deselectRow();
showRule('')
});
$('rulesTable').addEventListener('contextmenu', (e) => {
if (e.toElement.nodeName === 'DIV') {
rssDownloaderRulesTable.deselectAll();
rssDownloaderRulesTable.deselectRow();
rssDownloaderRuleContextMenu.updateMenuItems();
showRule('')
}
});
// get all categories and add to combobox
new Request.JSON({
url: 'api/v2/torrents/categories',
noCache: true,
method: 'get',
onSuccess: (response) => {
let combobox = $('assignCategoryCombobox');
for (let cat in response) {
let option = document.createElement('option');
option.text = option.value = cat;
combobox.add(option);
}
}
}).send();
// get all rss feed
new Request.JSON({
url: 'api/v2/rss/items',
noCache: true,
method: 'post',
data: {
withData: false
},
onSuccess: (response) => {
feedList = [];
let flatten = (root) => {
for (let child in root) {
if (root[child].uid !== undefined)
feedList.push({name: child, url: root[child].url});
else
flatten(root[child]);
}
}
flatten(response);
}
}).send();
$('savetoDifferentDir').addEvent('click', () => {
$('saveToText').disabled = !$('savetoDifferentDir').checked;
})
updateRulesList();
};
const updateRulesList = () => {
// get all rules
new Request.JSON({
url: 'api/v2/rss/rules',
noCache: true,
method: 'get',
onSuccess: (response) => {
rssDownloaderRulesTable.clear();
let rowCount = 0;
for (let rule in response) {
rssDownloaderRulesTable.updateRowData({
rowId: rowCount++,
checked: response[rule].enabled,
name: rule
});
}
rssDownloaderRulesTable.updateTable(false);
rulesList = response;
}
}).send();
}
const modifyRuleState = (rule, setting, newState, callback = () => {}) => {
rulesList[rule][setting] = newState;
new Request({
url: 'api/v2/rss/setRule',
noCache: true,
method: 'post',
data: {
ruleName: rule,
ruleDef: JSON.stringify(rulesList[rule])
},
onSuccess: () => {
callback();
}
}).send();
}
const addRule = () => {
new MochaUI.Window({
id: 'newRulePage',
title: 'QBT_TR(New rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'newrule.html',
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const renameRule = (rule) => {
new MochaUI.Window({
id: 'renameRulePage',
title: 'QBT_TR(Rule renaming)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'rename_rule.html?rule=' + encodeURIComponent(rule),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 100
});
};
const removeRule = (rules) => {
new MochaUI.Window({
id: 'removeRulePage',
title: 'QBT_TR(Rule deletion confirmation)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'confirmruledeletion.html?rules=' + encodeURIComponent(rules.join('|')),
scrollbars: false,
resizable: false,
maximizable: false,
width: 360,
height: 80
});
};
const clearDownloadedEpisodes = (rules) => {
new MochaUI.Window({
id: 'clearRulesPage',
title: 'QBT_TR(New rule name)QBT_TR[CONTEXT=AutomatedRssDownloader]',
loadMethod: 'iframe',
contentURL: 'confirmruleclear.html?rules=' + encodeURIComponent(rules.join('|')),
scrollbars: false,
resizable: false,
maximizable: false,
width: 350,
height: 85
});
}
const saveSettings = () => {
let lastSelectedRow = rssDownloaderRulesTable.selectedRows[rssDownloaderRulesTable.selectedRows.length - 1];
let rule = rssDownloaderRulesTable.rows[lastSelectedRow].full_data.name;
rulesList[rule].useRegex = $('useRegEx').checked;
rulesList[rule].mustContain = $('mustContainText').value;
rulesList[rule].mustNotContain = $('mustNotContainText').value;
rulesList[rule].episodeFilter = $('episodeFilterText').value;
rulesList[rule].smartFilter = $('useSmartFilter').checked;
rulesList[rule].assignedCategory = $('assignCategoryCombobox').value;
rulesList[rule].savePath = $('savetoDifferentDir').checked ? $('saveToText').value : '';
rulesList[rule].ignoreDays = parseInt($('ignoreDaysValue').value);
switch ($('addPausedCombobox').value) {
case 'default':
rulesList[rule].addPaused = null;
break;
case 'always':
rulesList[rule].addPaused = true;
break;
case 'never':
rulesList[rule].addPaused = false;
break;
}
switch ($('creatSubfolderCombobox').value) {
case 'default':
rulesList[rule].createSubfolder = null;
break;
case 'always':
rulesList[rule].createSubfolder = true;
break;
case 'never':
rulesList[rule].createSubfolder = false;
break;
}
rulesList[rule].affectedFeeds = rssDownloaderFeedSelectionTable.rows.filter((row) => row.full_data.checked)
.map((row) => row.full_data.url)
.getValues();
new Request({
url: 'api/v2/rss/setRule',
noCache: true,
method: 'post',
data: {
ruleName: rule,
ruleDef: JSON.stringify(rulesList[rule])
},
onSuccess: () => {
updateMatchingArticles(rule);
}
}).send();
}
const updateMatchingArticles = (ruleName) => {
new Request.JSON({
url: 'api/v2/rss/matchingArticles',
noCache: true,
method: 'post',
data: {
ruleName: ruleName
},
onSuccess: (response) => {
rssDownloaderArticlesTable.clear();
let rowCount = 0;
for (let feed in response) {
rssDownloaderArticlesTable.updateRowData({
rowId: rowCount++,
name: feed,
isFeed: true
});
response[feed].each((article) => {
rssDownloaderArticlesTable.updateRowData({
rowId: rowCount++,
name: article,
isFeed: false
});
});
}
rssDownloaderArticlesTable.updateTable(false);
}
}).send();
}
const showRule = (ruleName) => {
if (ruleName === '') {
// disable all
$('saveButton').disabled = true;
$('useRegEx').disabled = true;
$('mustContainText').disabled = true;
$('mustNotContainText').disabled = true;
$('episodeFilterText').disabled = true;
$('useSmartFilter').disabled = true;
$('assignCategoryCombobox').disabled = true;
$('savetoDifferentDir').disabled = true;
$('saveToText').disabled = true;
$('ignoreDaysValue').disabled = true;
$('addPausedCombobox').disabled = true;
$('creatSubfolderCombobox').disabled = true;
// reset all boxes
$('useRegEx').checked = false;
$('mustContainText').value = '';
$('mustNotContainText').value = '';
$('episodeFilterText').value = '';
$('useSmartFilter').checked = false;
$('assignCategoryCombobox').value = 'default';
$('savetoDifferentDir').checked = false;
$('saveToText').value = '';
$('ignoreDaysValue').value = 0;
$('lastMatchText').innerHTML = 'QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]';
$('addPausedCombobox').value = 'default';
$('creatSubfolderCombobox').value = 'default';
rssDownloaderFeedSelectionTable.clear();
rssDownloaderArticlesTable.clear();
$('mustContainText').title = '';
$('mustNotContainText').title = '';
$('episodeFilterText').title = '';
}
else {
// enable all
$('saveButton').disabled = false;
$('useRegEx').disabled = false;
$('mustContainText').disabled = false;
$('mustNotContainText').disabled = false;
$('episodeFilterText').disabled = false;
$('useSmartFilter').disabled = false;
$('assignCategoryCombobox').disabled = false;
$('savetoDifferentDir').disabled = false;
$('savetoDifferentDir').checked = rulesList[ruleName].savePath ? false : true;
$('saveToText').disabled = rulesList[ruleName].savePath ? false : true;
$('ignoreDaysValue').disabled = false;
$('addPausedCombobox').disabled = false;
$('creatSubfolderCombobox').disabled = false;
// load rule settings
$('useRegEx').checked = rulesList[ruleName].useRegex;
$('mustContainText').value = rulesList[ruleName].mustContain;
$('mustNotContainText').value = rulesList[ruleName].mustNotContain;
$('episodeFilterText').value = rulesList[ruleName].episodeFilter;
$('useSmartFilter').checked = rulesList[ruleName].smartFilter;
$('assignCategoryCombobox').value = rulesList[ruleName].assignedCategory ? rulesList[ruleName].assignedCategory : 'default';
$('savetoDifferentDir').checked = rulesList[ruleName].savePath !== '';
$('saveToText').value = rulesList[ruleName].savePath;
$('ignoreDaysValue').value = rulesList[ruleName].ignoreDays;
// calculate days since last match
if (rulesList[ruleName].lastMatch !== '') {
let timeDiffInMs = new Date().getTime() - new Date(rulesList[ruleName].lastMatch).getTime();
let daysAgo = Math.floor(timeDiffInMs / (1000 * 60 * 60 * 24)).toString();
$('lastMatchText').innerHTML = ' QBT_TR(Last Match: %1 days ago)QBT_TR[CONTEXT=AutomatedRssDownloader]'.replace('%1', daysAgo);
}
else {
$('lastMatchText').innerHTML = 'QBT_TR(Last Match: Unknown)QBT_TR[CONTEXT=AutomatedRssDownloader]';
}
if (rulesList[ruleName].addPaused === null)
$('addPausedCombobox').value = 'default';
else
$('addPausedCombobox').value = rulesList[ruleName].addPaused ? 'always' : 'never';
if (rulesList[ruleName].createSubfolder === null)
$('creatSubfolderCombobox').value = 'default';
else
$('creatSubfolderCombobox').value = rulesList[ruleName].createSubfolder ? 'always' : 'never';
setElementTitles();
rssDownloaderFeedSelectionTable.clear();
let rowCount = 0;
feedList.forEach((feed) => {
rssDownloaderFeedSelectionTable.updateRowData({
rowId: rowCount++,
checked: rulesList[ruleName].affectedFeeds.contains(feed.url),
name: feed.name,
url: feed.url
});
});
rssDownloaderFeedSelectionTable.updateTable(false);
updateMatchingArticles(ruleName);
}
}
const setElementTitles = () => {
let mainPart;
if ($('useRegEx').checked) {
mainPart = 'QBT_TR(Regex mode: use Perl-compatible regular expressions)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n';
}
else {
mainPart = 'QBT_TR(Wildcard mode: you can use)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
' ● QBT_TR(? to match any single character)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(* to match zero or more of any characters)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Whitespaces count as AND operators (all words, any order))QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(| is used as OR operator)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(If word order is important use * instead of whitespace.)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n';
}
let secondPart = 'QBT_TR(An expression with an empty %1 clause (e.g. %2))QBT_TR[CONTEXT=AutomatedRssDownloader]'
.replace('%1', '|').replace('%2', 'expr|');
$('mustContainText').title = mainPart + secondPart + 'QBT_TR( will match all articles.)QBT_TR[CONTEXT=AutomatedRssDownloader]';
$('mustNotContainText').title = mainPart + secondPart + 'QBT_TR( will exclude all articles.)QBT_TR[CONTEXT=AutomatedRssDownloader]';
let episodeFilterTitle = 'QBT_TR(Matches articles based on episode filter.)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(Example: )QBT_TR[CONTEXT=AutomatedRssDownloader]' +
'1x2;8-15;5;30-;' +
'QBT_TR( will match 2, 5, 8 through 15, 30 and onward episodes of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
'QBT_TR(Episode filter rules: )QBT_TR[CONTEXT=AutomatedRssDownloader]\n\n' +
' ● QBT_TR(Season number is a mandatory non-zero value)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Episode number is a mandatory positive value)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Filter must end with semicolon)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Three range types for episodes are supported: )QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Single number: <b>1x25;</b> matches episode 25 of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Normal range: <b>1x25-40;</b> matches episodes 25 through 40 of season one)QBT_TR[CONTEXT=AutomatedRssDownloader]\n' +
' ● QBT_TR(Infinite range: <b>1x25-;</b> matches episodes 25 and upward of season one, and all episodes of later seasons)QBT_TR[CONTEXT=AutomatedRssDownloader]';
episodeFilterTitle = episodeFilterTitle.replace(/<b>/g, '').replace(/<\/b>/g, '');
$('episodeFilterText').title = episodeFilterTitle;
}
initRssDownloader();
return exports();
})();
</script>

10
src/webui/www/webui.qrc

@ -3,6 +3,9 @@
<file>private/addpeers.html</file> <file>private/addpeers.html</file>
<file>private/addtrackers.html</file> <file>private/addtrackers.html</file>
<file>private/confirmdeletion.html</file> <file>private/confirmdeletion.html</file>
<file>private/confirmfeeddeletion.html</file>
<file>private/confirmruleclear.html</file>
<file>private/confirmruledeletion.html</file>
<file>private/css/Core.css</file> <file>private/css/Core.css</file>
<file>private/css/dynamicTable.css</file> <file>private/css/dynamicTable.css</file>
<file>private/css/Layout.css</file> <file>private/css/Layout.css</file>
@ -33,9 +36,14 @@
<file>private/images/toolbox-divider.gif</file> <file>private/images/toolbox-divider.gif</file>
<file>private/index.html</file> <file>private/index.html</file>
<file>private/newcategory.html</file> <file>private/newcategory.html</file>
<file>private/newfeed.html</file>
<file>private/newfolder.html</file>
<file>private/newrule.html</file>
<file>private/newtag.html</file> <file>private/newtag.html</file>
<file>private/rename.html</file> <file>private/rename.html</file>
<file>private/rename_feed.html</file>
<file>private/rename_file.html</file> <file>private/rename_file.html</file>
<file>private/rename_rule.html</file>
<file>private/scripts/client.js</file> <file>private/scripts/client.js</file>
<file>private/scripts/contextmenu.js</file> <file>private/scripts/contextmenu.js</file>
<file>private/scripts/download.js</file> <file>private/scripts/download.js</file>
@ -68,6 +76,8 @@
<file>private/views/preferencesToolbar.html</file> <file>private/views/preferencesToolbar.html</file>
<file>private/views/properties.html</file> <file>private/views/properties.html</file>
<file>private/views/propertiesToolbar.html</file> <file>private/views/propertiesToolbar.html</file>
<file>private/views/rss.html</file>
<file>private/views/rssDownloader.html</file>
<file>private/views/search.html</file> <file>private/views/search.html</file>
<file>private/views/searchplugins.html</file> <file>private/views/searchplugins.html</file>
<file>private/views/statistics.html</file> <file>private/views/statistics.html</file>

Loading…
Cancel
Save