Mike Tzou
5 years ago
committed by
GitHub
18 changed files with 2902 additions and 27 deletions
@ -0,0 +1,55 @@
@@ -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> |
@ -0,0 +1,48 @@
@@ -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> |
@ -0,0 +1,56 @@
@@ -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> |
@ -0,0 +1,78 @@
@@ -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> |
@ -0,0 +1,77 @@
@@ -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> |
@ -0,0 +1,70 @@
@@ -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> |
@ -0,0 +1,88 @@
@@ -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> |
@ -0,0 +1,81 @@
@@ -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> |
@ -0,0 +1,823 @@
@@ -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> |
@ -0,0 +1,735 @@
@@ -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> |
Loading…
Reference in new issue