Browse Source

Implement tag management for WebUI

adaptive-webui-19844
Vasiliy Halimonchuk 6 years ago
parent
commit
b530e19a44
  1. 6
      src/webui/api/synccontroller.cpp
  2. 64
      src/webui/api/torrentscontroller.cpp
  3. 5
      src/webui/api/torrentscontroller.h
  4. 5
      src/webui/www/private/css/style.css
  5. 42
      src/webui/www/private/filters.html
  6. 12
      src/webui/www/private/index.html
  7. 105
      src/webui/www/private/newtag.html
  8. 142
      src/webui/www/private/scripts/client.js
  9. 68
      src/webui/www/private/scripts/contextmenu.js
  10. 34
      src/webui/www/private/scripts/dynamicTable.js
  11. 158
      src/webui/www/private/scripts/mocha-init.js
  12. 1
      src/webui/www/webui.qrc

6
src/webui/api/synccontroller.cpp

@ -445,6 +445,12 @@ void SyncController::maindataAction()
data["categories"] = categories; data["categories"] = categories;
QVariantList tags;
for (const QString &tag : asConst(session->tags()))
tags << tag;
data["tags"] = tags;
QVariantMap serverState = getTranserInfo(); QVariantMap serverState = getTranserInfo();
serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace(); serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace();
serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled();

64
src/webui/api/torrentscontroller.cpp

@ -1041,3 +1041,67 @@ void TorrentsController::categoriesAction()
setResult(categories); setResult(categories);
} }
void TorrentsController::addTagsAction()
{
checkParams({"hashes", "tags"});
const QStringList hashes {params()["hashes"].split('|')};
const QStringList tags {params()["tags"].split(',', QString::SkipEmptyParts)};
for (const QString &tag : tags) {
const QString tagTrimmed {tag.trimmed()};
applyToTorrents(hashes, [&tagTrimmed](BitTorrent::TorrentHandle *const torrent)
{
torrent->addTag(tagTrimmed);
});
}
}
void TorrentsController::removeTagsAction()
{
checkParams({"hashes"});
const QStringList hashes {params()["hashes"].split('|')};
const QStringList tags {params()["tags"].split(',', QString::SkipEmptyParts)};
for (const QString &tag : tags) {
const QString tagTrimmed {tag.trimmed()};
applyToTorrents(hashes, [&tagTrimmed](BitTorrent::TorrentHandle *const torrent)
{
torrent->removeTag(tagTrimmed);
});
}
if (tags.isEmpty()) {
applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent)
{
torrent->removeAllTags();
});
}
}
void TorrentsController::createTagsAction()
{
checkParams({"tags"});
const QStringList tags {params()["tags"].split(',', QString::SkipEmptyParts)};
for (const QString &tag : tags)
BitTorrent::Session::instance()->addTag(tag.trimmed());
}
void TorrentsController::deleteTagsAction()
{
checkParams({"tags"});
const QStringList tags {params()["tags"].split(',', QString::SkipEmptyParts)};
for (const QString &tag : tags)
BitTorrent::Session::instance()->removeTag(tag.trimmed());
}
void TorrentsController::tagsAction()
{
const QStringList tags = BitTorrent::Session::instance()->tags().toList();
setResult(QJsonArray::fromStringList(tags));
}

5
src/webui/api/torrentscontroller.h

@ -56,6 +56,11 @@ private slots:
void editCategoryAction(); void editCategoryAction();
void removeCategoriesAction(); void removeCategoriesAction();
void categoriesAction(); void categoriesAction();
void addTagsAction();
void removeTagsAction();
void createTagsAction();
void deleteTagsAction();
void tagsAction();
void addAction(); void addAction();
void deleteAction(); void deleteAction();
void addTrackersAction(); void addTrackersAction();

5
src/webui/www/private/css/style.css

@ -258,6 +258,11 @@ a.propButton img {
-ms-interpolation-mode: bicubic; -ms-interpolation-mode: bicubic;
} }
.contextMenu li input[type=checkbox] {
position: relative;
top: 3px;
}
/* Sliders */ /* Sliders */
.slider { .slider {

42
src/webui/www/private/filters.html

@ -21,6 +21,13 @@
<ul class="filterList" id="categoryFilterList"> <ul class="filterList" id="categoryFilterList">
</ul> </ul>
</div> </div>
<div class="filterWrapper">
<span class="filterTitle" onclick="toggleFilterDisplay('tag');">
<img src="images/qbt-theme/go-down.svg">QBT_TR(Tags)QBT_TR[CONTEXT=TransferListFiltersWidget]
</span>
<ul class="filterList" id="tagFilterList">
</ul>
</div>
<script> <script>
'use strict'; 'use strict';
@ -60,9 +67,44 @@
} }
}); });
const tagsFilterContextMenu = new TagsFilterContextMenu({
targets: '.tagsFilterContextMenuTarget',
menu: 'tagsFilterMenu',
actions: {
createTag: function(element, ref) {
createTagFN();
},
deleteTag: function(element, ref) {
removeTagFN(element.id);
},
deleteUnusedTags: function(element, ref) {
deleteUnusedTagsFN();
},
startTorrentsByTag: function(element, ref) {
startTorrentsByTagFN(element.id);
},
pauseTorrentsByTag: function(element, ref) {
pauseTorrentsByTagFN(element.id);
},
deleteTorrentsByTag: function(element, ref) {
deleteTorrentsByTagFN(element.id);
}
},
offsets: {
x: -15,
y: 2
},
onShow: function() {
this.options.element.firstChild.click();
}
});
if (localStorage.getItem('filter_status_collapsed') === "true") if (localStorage.getItem('filter_status_collapsed') === "true")
toggleFilterDisplay('status'); toggleFilterDisplay('status');
if (localStorage.getItem('filter_category_collapsed') === "true") if (localStorage.getItem('filter_category_collapsed') === "true")
toggleFilterDisplay('category'); toggleFilterDisplay('category');
if (localStorage.getItem('filter_tag_collapsed') === "true")
toggleFilterDisplay('tag');
</script> </script>

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

@ -132,6 +132,10 @@
<a href="#Category" class="arrow-right"><img src="images/qbt-theme/view-categories.svg" alt="QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]</a> <a href="#Category" class="arrow-right"><img src="images/qbt-theme/view-categories.svg" alt="QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]</a>
<ul id="contextCategoryList" class="scrollableMenu"></ul> <ul id="contextCategoryList" class="scrollableMenu"></ul>
</li> </li>
<li>
<a href="#Tags" class="arrow-right"><img src="images/qbt-theme/view-categories.svg" alt="QBT_TR(Tags)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Tags)QBT_TR[CONTEXT=TransferListWidget]</a>
<ul id="contextTagList" class="scrollableMenu"></ul>
</li>
<li> <li>
<a href="#autoTorrentManagement"><img src="images/qbt-theme/checked.svg" alt="QBT_TR(Automatic Torrent Management)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Automatic Torrent Management)QBT_TR[CONTEXT=TransferListWidget]</a> <a href="#autoTorrentManagement"><img src="images/qbt-theme/checked.svg" alt="QBT_TR(Automatic Torrent Management)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Automatic Torrent Management)QBT_TR[CONTEXT=TransferListWidget]</a>
</li> </li>
@ -170,6 +174,14 @@
<li><a href="#pauseTorrentsByCategory"><img src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget]"/> QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget]</a></li> <li><a href="#pauseTorrentsByCategory"><img src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget]"/> QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget]</a></li>
<li><a href="#deleteTorrentsByCategory"><img src="images/qbt-theme/edit-delete.svg" alt="QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget]"/> QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget]</a></li> <li><a href="#deleteTorrentsByCategory"><img src="images/qbt-theme/edit-delete.svg" alt="QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget]"/> QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget]</a></li>
</ul> </ul>
<ul id="tagsFilterMenu" class="contextMenu">
<li><a href="#createTag"><img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add tag...)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Add tag...)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
<li><a href="#deleteTag"><img src="images/qbt-theme/list-remove.svg" alt="QBT_TR(Remove tag)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Remove tag)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
<li><a href="#deleteUnusedTags"><img src="images/qbt-theme/list-remove.svg" alt="QBT_TR(Remove unused tags)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Remove unused tags)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
<li class="separator"><a href="#startTorrentsByTag"><img src="images/qbt-theme/media-playback-start.svg" alt="QBT_TR(Resume torrents)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Resume torrents)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
<li><a href="#pauseTorrentsByTag"><img src="images/qbt-theme/media-playback-pause.svg" alt="QBT_TR(Pause torrents)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Pause torrents)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
<li><a href="#deleteTorrentsByTag"><img src="images/qbt-theme/edit-delete.svg" alt="QBT_TR(Delete torrents)QBT_TR[CONTEXT=TagFilterWidget]"/> QBT_TR(Delete torrents)QBT_TR[CONTEXT=TagFilterWidget]</a></li>
</ul>
<ul id="torrentTrackersMenu" class="contextMenu"> <ul id="torrentTrackersMenu" class="contextMenu">
<li><a href="#AddTracker"><img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add a new tracker...)QBT_TR[CONTEXT=TrackerListWidget]"/> QBT_TR(Add a new tracker...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#AddTracker"><img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add a new tracker...)QBT_TR[CONTEXT=TrackerListWidget]"/> QBT_TR(Add a new tracker...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li class="separator"><a href="#EditTracker"><img src="images/qbt-theme/document-edit.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"/> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li class="separator"><a href="#EditTracker"><img src="images/qbt-theme/document-edit.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"/> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>

105
src/webui/www/private/newtag.html

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Add Tags)QBT_TR[CONTEXT=TransferListWidget]</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': function(event) {
$('tagNameButton').click();
event.preventDefault();
},
'Escape': function(event) {
window.parent.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
window.parent.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', function() {
const uriAction = safeTrim(new URI().getData('action'));
const uriHashes = safeTrim(new URI().getData('hashes'));
if (uriAction === 'create')
$('legendText').innerText = 'QBT_TR(Tag:)QBT_TR[CONTEXT=TagFilterWidget]';
$('tagName').focus();
$('tagNameButton').addEvent('click', function(e) {
new Event(e).stop();
const tagName = $('tagName').value.trim();
const verifyTagName = function(name) {
if ((name === null) || (name === ""))
return false;
if (name.indexOf(",") >= 0) {
alert("QBT_TR(Invalid tag name)QBT_TR[CONTEXT=TagFilterWidget]");
return false;
}
return true;
};
switch (uriAction) {
case "set":
if (uriHashes === "")
return;
new Request({
url: 'api/v2/torrents/addTags',
method: 'post',
data: {
hashes: uriHashes,
tags: tagName,
},
onComplete: function() {
window.parent.closeWindows();
}
}).send();
break;
case "create":
if (!verifyTagName(tagName))
return;
new Request({
url: 'api/v2/torrents/createTags',
method: 'post',
data: {
tags: tagName,
},
onComplete: function() {
window.parent.closeWindows();
}
}).send();
break;
}
});
});
</script>
</head>
<body>
<div style="padding: 10px 10px 0px 10px;">
<p id="legendText" style="font-weight: bold;">QBT_TR(Comma-separated tags:)QBT_TR[CONTEXT=TransferListWidget]</p>
<input type="text" id="tagName" value="" maxlength="100" style="width: 220px;" />
<div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(Add)QBT_TR[CONTEXT=HttpServer]" id="tagNameButton" />
</div>
</div>
</body>
</html>

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

@ -55,6 +55,14 @@ var category_list = {};
var selected_category = CATEGORIES_ALL; var selected_category = CATEGORIES_ALL;
var setCategoryFilter = function() {}; var setCategoryFilter = function() {};
const TAGS_ALL = 1;
const TAGS_UNTAGGED = 2;
let tagList = {};
let selectedTag = TAGS_ALL;
let setTagFilter = function() {};
var selected_filter = getLocalStorageItem('selected_filter', 'all'); var selected_filter = getLocalStorageItem('selected_filter', 'all');
var setFilter = function() {}; var setFilter = function() {};
var toggleFilterDisplay = function() {}; var toggleFilterDisplay = function() {};
@ -64,6 +72,11 @@ var loadSelectedCategory = function() {
}; };
loadSelectedCategory(); loadSelectedCategory();
const loadSelectedTag = function() {
selectedTag = getLocalStorageItem('selected_tag', TAGS_ALL);
};
loadSelectedTag();
function genHash(string) { function genHash(string) {
var hash = 0; var hash = 0;
for (var i = 0; i < string.length; ++i) { for (var i = 0; i < string.length; ++i) {
@ -161,6 +174,14 @@ window.addEvent('load', function() {
updateMainData(); updateMainData();
}; };
setTagFilter = function(hash) {
selectedTag = hash.toString();
localStorage.setItem('selected_tag', selectedTag);
highlightSelectedTag();
if (torrentsTable.tableBody !== undefined)
updateMainData();
};
setFilter = function(f) { setFilter = function(f) {
// Visually Select the right filter // Visually Select the right filter
$("all_filter").removeClass("selectedFilter"); $("all_filter").removeClass("selectedFilter");
@ -282,8 +303,44 @@ window.addEvent('load', function() {
return false; return false;
}; };
const removeTorrentFromTagList = function(hash) {
if ((hash === null) || (hash === ""))
return false;
let removed = false;
for (const key in tagList) {
const tag = tagList[key];
if (Object.contains(tag.torrents, hash)) {
removed = true;
tag.torrents.splice(tag.torrents.indexOf(hash), 1);
}
}
return removed;
};
const addTorrentToTagList = function(torrent) {
if (torrent['tags'] === undefined) // Tags haven't changed
return false;
removeTorrentFromTagList(torrent['hash']);
if (torrent['tags'].length === 0) // No tags
return true;
const tags = torrent['tags'].split(',');
let added = false;
for (let i = 0; i < tags.length; ++i) {
const tagHash = genHash(tags[i].trim());
if (!Object.contains(tagList[tagHash].torrents, torrent['hash'])) {
added = true;
tagList[tagHash].torrents.push(torrent['hash']);
}
}
return added;
};
var updateFilter = function(filter, filterTitle) { var updateFilter = function(filter, filterTitle) {
$(filter + '_filter').firstChild.childNodes[1].nodeValue = filterTitle.replace('%1', torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL)); $(filter + '_filter').firstChild.childNodes[1].nodeValue = filterTitle.replace('%1', torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL));
}; };
var updateFiltersList = function() { var updateFiltersList = function() {
@ -353,6 +410,60 @@ window.addEvent('load', function() {
} }
}; };
const updateTagList = function() {
const tagFilterList = $('tagFilterList');
if (tagFilterList === null)
return;
while (tagFilterList.firstChild !== null)
tagFilterList.removeChild(tagFilterList.firstChild);
const createLink = function(hash, text, count) {
const html = '<a href="#" onclick="setTagFilter(' + hash + ');return false;">'
+ '<img src="images/qbt-theme/inode-directory.svg"/>'
+ escapeHtml(text) + ' (' + count + ')' + '</a>';
const el = new Element('li', {
id: hash,
html: html
});
tagsFilterContextMenu.addTarget(el);
return el;
};
const torrentsCount = torrentsTable.getRowIds().length;
let untagged = 0;
for (const key in torrentsTable.rows) {
if (torrentsTable.rows.hasOwnProperty(key) && torrentsTable.rows[key]['full_data'].tags.length === 0)
untagged += 1;
}
tagFilterList.appendChild(createLink(TAGS_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]', torrentsCount));
tagFilterList.appendChild(createLink(TAGS_UNTAGGED, 'QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]', untagged));
const sortedTags = [];
for (const key in tagList)
sortedTags.push(tagList[key].name);
sortedTags.sort();
for (let i = 0; i < sortedTags.length; ++i) {
const tagName = sortedTags[i];
const tagHash = genHash(tagName);
const tagCount = tagList[tagHash].torrents.length;
tagFilterList.appendChild(createLink(tagHash, tagName, tagCount));
}
highlightSelectedTag();
};
const highlightSelectedTag = function() {
const tagFilterList = $('tagFilterList');
if (!tagFilterList)
return;
const children = tagFilterList.childNodes;
for (let i = 0; i < children.length; ++i)
children[i].className = (children[i].id === selectedTag) ? "selectedFilter" : "";
};
var syncMainDataTimer; var syncMainDataTimer;
var syncMainData = function() { var syncMainData = function() {
var url = new URI('api/v2/sync/maindata'); var url = new URI('api/v2/sync/maindata');
@ -374,11 +485,13 @@ window.addEvent('load', function() {
clearTimeout(torrentsFilterInputTimer); clearTimeout(torrentsFilterInputTimer);
var torrentsTableSelectedRows; var torrentsTableSelectedRows;
var update_categories = false; var update_categories = false;
let updateTags = false;
var full_update = (response['full_update'] === true); var full_update = (response['full_update'] === true);
if (full_update) { if (full_update) {
torrentsTableSelectedRows = torrentsTable.selectedRowsIds(); torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
torrentsTable.clear(); torrentsTable.clear();
category_list = {}; category_list = {};
tagList = {};
} }
if (response['rid']) { if (response['rid']) {
syncMainDataLastResponseId = response['rid']; syncMainDataLastResponseId = response['rid'];
@ -408,6 +521,25 @@ window.addEvent('load', function() {
}); });
update_categories = true; update_categories = true;
} }
if (response['tags']) {
for (const tag of response['tags']) {
const tagHash = genHash(tag);
if (!tagList[tagHash]) {
tagList[tagHash] = {
name: tag,
torrents: []
};
}
}
updateTags = true;
}
if (response['tags_removed']) {
for (let i = 0; i < response['tags_removed'].length; ++i) {
const tagHash = genHash(response['tags_removed'][i]);
delete tagList[tagHash];
}
updateTags = true;
}
if (response['torrents']) { if (response['torrents']) {
var updateTorrentList = false; var updateTorrentList = false;
for (var key in response['torrents']) { for (var key in response['torrents']) {
@ -418,6 +550,8 @@ window.addEvent('load', function() {
torrentsTable.updateRowData(response['torrents'][key]); torrentsTable.updateRowData(response['torrents'][key]);
if (addTorrentToCategoryList(response['torrents'][key])) if (addTorrentToCategoryList(response['torrents'][key]))
update_categories = true; update_categories = true;
if (addTorrentToTagList(response['torrents'][key]))
updateTags = true;
if (response['torrents'][key]['name']) if (response['torrents'][key]['name'])
updateTorrentList = true; updateTorrentList = true;
} }
@ -430,6 +564,8 @@ window.addEvent('load', function() {
torrentsTable.removeRow(hash); torrentsTable.removeRow(hash);
removeTorrentFromCategoryList(hash); removeTorrentFromCategoryList(hash);
update_categories = true; // Always to update All category update_categories = true; // Always to update All category
removeTorrentFromTagList(hash);
updateTags = true; // Always to update All tag
}); });
torrentsTable.updateTable(full_update); torrentsTable.updateTable(full_update);
torrentsTable.altRow(); torrentsTable.altRow();
@ -444,6 +580,10 @@ window.addEvent('load', function() {
updateCategoryList(); updateCategoryList();
torrentsTableContextMenu.updateCategoriesSubMenu(category_list); torrentsTableContextMenu.updateCategoriesSubMenu(category_list);
} }
if (updateTags) {
updateTagList();
torrentsTableContextMenu.updateTagsSubMenu(tagList);
}
if (full_update) if (full_update)
// re-select previously selected rows // re-select previously selected rows

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

@ -293,6 +293,7 @@ var TorrentsTableContextMenu = new Class({
var all_are_super_seeding = true; var all_are_super_seeding = true;
var all_are_auto_tmm = true; var all_are_auto_tmm = true;
var there_are_auto_tmm = false; var there_are_auto_tmm = false;
const tagsSelectionState = Object.clone(tagList);
var h = torrentsTable.selectedRowsIds(); var h = torrentsTable.selectedRowsIds();
h.each(function(item, index) { h.each(function(item, index) {
@ -327,6 +328,18 @@ var TorrentsTableContextMenu = new Class({
there_are_auto_tmm = true; there_are_auto_tmm = true;
else else
all_are_auto_tmm = false; all_are_auto_tmm = false;
const torrentTags = data['tags'].split(', ');
for (const key in tagsSelectionState) {
const tag = tagsSelectionState[key];
const tagExists = torrentTags.contains(tag.name);
if ((tag.checked !== undefined) && (tag.checked != tagExists))
tag.indeterminate = true;
if (tag.checked === undefined)
tag.checked = tagExists;
else
tag.checked = tag.checked && tagExists;
}
}); });
var show_seq_dl = true; var show_seq_dl = true;
@ -389,6 +402,13 @@ var TorrentsTableContextMenu = new Class({
this.setItemChecked('autoTorrentManagement', all_are_auto_tmm); this.setItemChecked('autoTorrentManagement', all_are_auto_tmm);
} }
const contextTagList = $('contextTagList');
for (const tagHash in tagList) {
const checkbox = contextTagList.getElement('a[href=#Tag/' + tagHash + '] input[type=checkbox]');
const checkboxState = tagsSelectionState[tagHash];
checkbox.indeterminate = checkboxState.indeterminate;
checkbox.checked = checkboxState.checked;
}
}, },
updateCategoriesSubMenu: function(category_list) { updateCategoriesSubMenu: function(category_list) {
@ -419,6 +439,43 @@ var TorrentsTableContextMenu = new Class({
} }
categoryList.appendChild(el); categoryList.appendChild(el);
}); });
},
updateTagsSubMenu: function(tagList) {
const contextTagList = $('contextTagList');
while (contextTagList.firstChild !== null)
contextTagList.removeChild(contextTagList.firstChild);
contextTagList.appendChild(new Element('li', {
html: '<a href="javascript:torrentAddTagsFN();">'
+ '<img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ ' QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]'
+ '</a>'
}));
contextTagList.appendChild(new Element('li', {
html: '<a href="javascript:torrentRemoveAllTagsFN();">'
+ '<img src="images/qbt-theme/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ ' QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]'
+ '</a>'
}));
const sortedTags = [];
for (const key in tagList)
sortedTags.push(tagList[key].name);
sortedTags.sort();
for (let i = 0; i < sortedTags.length; ++i) {
const tagName = sortedTags[i];
const tagHash = genHash(tagName);
const el = new Element('li', {
html: '<a href="#Tag/' + tagHash + '" onclick="event.preventDefault(); torrentSetTagsFN(\'' + tagHash + '\', !event.currentTarget.getElement(\'input[type=checkbox]\').checked);">'
+ '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + escapeHtml(tagName)
+ '</a>'
});
if (i === 0)
el.addClass('separator');
contextTagList.appendChild(el);
}
} }
}); });
@ -437,6 +494,17 @@ var CategoriesFilterContextMenu = new Class({
} }
}); });
const TagsFilterContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
const id = this.options.element.id;
if ((id !== TAGS_ALL.toString()) && (id !== TAGS_UNTAGGED.toString()))
this.showItem('deleteTag');
else
this.hideItem('deleteTag');
}
});
var SearchPluginsTableContextMenu = new Class({ var SearchPluginsTableContextMenu = new Class({
Extends: ContextMenu, Extends: ContextMenu,

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

@ -1183,7 +1183,7 @@ var TorrentsTable = new Class({
}; };
}, },
applyFilter: function(row, filterName, categoryHash, filterTerms) { applyFilter: function(row, filterName, categoryHash, tagHash, filterTerms) {
var state = row['full_data'].state; var state = row['full_data'].state;
var name = row['full_data'].name.toLowerCase(); var name = row['full_data'].name.toLowerCase();
var inactive = false; var inactive = false;
@ -1242,6 +1242,28 @@ var TorrentsTable = new Class({
} }
} }
const tagHashInt = parseInt(tagHash);
const isNumber = !isNaN(tagHashInt);
if (isNumber) {
switch (tagHashInt) {
case TAGS_ALL:
break; // do nothing
case TAGS_UNTAGGED:
if (row['full_data'].tags.length !== 0)
return false;
break; // do nothing
default:
let rowTags = row['full_data'].tags.split(', ');
rowTags = rowTags.map(function(tag) {
return genHash(tag);
});
if (!rowTags.contains(tagHashInt))
return false;
}
}
if (filterTerms) { if (filterTerms) {
for (var i = 0; i < filterTerms.length; ++i) { for (var i = 0; i < filterTerms.length; ++i) {
if (name.indexOf(filterTerms[i]) === -1) if (name.indexOf(filterTerms[i]) === -1)
@ -1252,21 +1274,21 @@ var TorrentsTable = new Class({
return true; return true;
}, },
getFilteredTorrentsNumber: function(filterName, categoryHash) { getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash) {
var cnt = 0; var cnt = 0;
var rows = this.rows.getValues(); var rows = this.rows.getValues();
for (var i = 0; i < rows.length; ++i) for (var i = 0; i < rows.length; ++i)
if (this.applyFilter(rows[i], filterName, categoryHash, null)) ++cnt; if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, null)) ++cnt;
return cnt; return cnt;
}, },
getFilteredTorrentsHashes: function(filterName, categoryHash) { getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash) {
var rowsHashes = []; var rowsHashes = [];
var rows = this.rows.getValues(); var rows = this.rows.getValues();
for (var i = 0; i < rows.length; ++i) for (var i = 0; i < rows.length; ++i)
if (this.applyFilter(rows[i], filterName, categoryHash, null)) if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, null))
rowsHashes.push(rows[i]['rowId']); rowsHashes.push(rows[i]['rowId']);
return rowsHashes; return rowsHashes;
@ -1280,7 +1302,7 @@ var TorrentsTable = new Class({
var filterTerms = (filterText.length > 0) ? filterText.split(" ") : null; var filterTerms = (filterText.length > 0) ? filterText.split(" ") : null;
for (var i = 0; i < rows.length; ++i) { for (var i = 0; i < rows.length; ++i) {
if (this.applyFilter(rows[i], selected_filter, selected_category, filterTerms)) { if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, filterTerms)) {
filteredRows.push(rows[i]); filteredRows.push(rows[i]);
filteredRows[rows[i].rowId] = rows[i]; filteredRows[rows[i].rowId] = rows[i];
} }

158
src/webui/www/private/scripts/mocha-init.js

@ -90,6 +90,15 @@ var deleteUnusedCategoriesFN = function() {};
var startTorrentsByCategoryFN = function() {}; var startTorrentsByCategoryFN = function() {};
var pauseTorrentsByCategoryFN = function() {}; var pauseTorrentsByCategoryFN = function() {};
var deleteTorrentsByCategoryFN = function() {}; var deleteTorrentsByCategoryFN = function() {};
let torrentAddTagsFN = function() {};
let torrentSetTagsFN = function() {};
let torrentRemoveAllTagsFN = function() {};
let createTagFN = function() {};
let removeTagFN = function() {};
let deleteUnusedTagsFN = function() {};
let startTorrentsByTagFN = function() {};
let pauseTorrentsByTagFN = function() {};
let deleteTorrentsByTagFN = function() {};
var copyNameFN = function() {}; var copyNameFN = function() {};
var copyMagnetLinkFN = function() {}; var copyMagnetLinkFN = function() {};
var copyHashFN = function() {}; var copyHashFN = function() {};
@ -616,7 +625,7 @@ var initializeWindows = function() {
deleteUnusedCategoriesFN = function() { deleteUnusedCategoriesFN = function() {
var categories = []; var categories = [];
for (var hash in category_list) { for (var hash in category_list) {
if (torrentsTable.getFilteredTorrentsNumber('all', hash) === 0) if (torrentsTable.getFilteredTorrentsNumber('all', hash, TAGS_ALL) === 0)
categories.push(category_list[hash].name); categories.push(category_list[hash].name);
} }
new Request({ new Request({
@ -630,7 +639,7 @@ var initializeWindows = function() {
}; };
startTorrentsByCategoryFN = function(categoryHash) { startTorrentsByCategoryFN = function(categoryHash) {
var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash); var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/resume', url: 'api/v2/torrents/resume',
@ -644,7 +653,7 @@ var initializeWindows = function() {
}; };
pauseTorrentsByCategoryFN = function(categoryHash) { pauseTorrentsByCategoryFN = function(categoryHash) {
var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash); var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/pause', url: 'api/v2/torrents/pause',
@ -658,7 +667,148 @@ var initializeWindows = function() {
}; };
deleteTorrentsByCategoryFN = function(categoryHash) { deleteTorrentsByCategoryFN = function(categoryHash) {
var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash); var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL);
if (hashes.length) {
new MochaUI.Window({
id: 'confirmDeletionPage',
title: "QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=confirmDeletionDlg]",
loadMethod: 'iframe',
contentURL: 'confirmdeletion.html?hashes=' + hashes.join("|"),
scrollbars: false,
resizable: false,
maximizable: false,
padding: 10,
width: 424,
height: 140
});
updateMainData();
}
};
torrentAddTagsFN = function() {
const action = "set";
const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) {
new MochaUI.Window({
id: 'newTagPage',
title: "QBT_TR(Add Tags)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe',
contentURL: 'newtag.html?action=' + action + '&hashes=' + hashes.join('|'),
scrollbars: false,
resizable: false,
maximizable: false,
paddingVertical: 0,
paddingHorizontal: 0,
width: 250,
height: 100
});
}
};
torrentSetTagsFN = function(tagHash, isSet) {
const tagName = ((tagHash === '0') ? '' : tagList[tagHash].name);
const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) {
new Request({
url: (isSet ? 'api/v2/torrents/addTags' : 'api/v2/torrents/removeTags'),
method: 'post',
data: {
hashes: hashes.join("|"),
tags: tagName,
}
}).send();
}
};
torrentRemoveAllTagsFN = function() {
const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) {
new Request({
url: ('api/v2/torrents/removeTags'),
method: 'post',
data: {
hashes: hashes.join("|"),
}
}).send();
}
};
createTagFN = function() {
const action = "create";
new MochaUI.Window({
id: 'newTagPage',
title: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]",
loadMethod: 'iframe',
contentURL: 'newtag.html?action=' + action,
scrollbars: false,
resizable: false,
maximizable: false,
paddingVertical: 0,
paddingHorizontal: 0,
width: 250,
height: 100
});
updateMainData();
};
removeTagFN = function(tagHash) {
const tagName = tagList[tagHash].name;
new Request({
url: 'api/v2/torrents/deleteTags',
method: 'post',
data: {
tags: tagName
}
}).send();
setTagFilter(TAGS_ALL);
};
deleteUnusedTagsFN = function() {
const tags = [];
for (const hash in tagList) {
if (torrentsTable.getFilteredTorrentsNumber('all', CATEGORIES_ALL, hash) === 0)
tags.push(tagList[hash].name);
}
new Request({
url: 'api/v2/torrents/deleteTags',
method: 'post',
data: {
tags: tags.join(',')
}
}).send();
setTagFilter(TAGS_ALL);
};
startTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash);
if (hashes.length) {
new Request({
url: 'api/v2/torrents/resume',
method: 'post',
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
pauseTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash);
if (hashes.length) {
new Request({
url: 'api/v2/torrents/pause',
method: 'post',
data: {
hashes: hashes.join("|")
}
}).send();
updateMainData();
}
};
deleteTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash);
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'confirmDeletionPage', id: 'confirmDeletionPage',

1
src/webui/www/webui.qrc

@ -18,6 +18,7 @@
<file>private/index.html</file> <file>private/index.html</file>
<file>private/installsearchplugin.html</file> <file>private/installsearchplugin.html</file>
<file>private/newcategory.html</file> <file>private/newcategory.html</file>
<file>private/newtag.html</file>
<file>private/preferences.html</file> <file>private/preferences.html</file>
<file>private/preferences_content.html</file> <file>private/preferences_content.html</file>
<file>private/properties.html</file> <file>private/properties.html</file>

Loading…
Cancel
Save