mirror of
https://github.com/transmission/transmission
synced 2025-03-04 10:38:13 +00:00
Add labels support into web client. (#2596)
This commit is contained in:
parent
663fba71c2
commit
0973cfd96d
10 changed files with 199 additions and 15 deletions
File diff suppressed because one or more lines are too long
|
@ -45,6 +45,10 @@ export class ActionManager extends EventTarget {
|
|||
shortcut: 'Alt+I',
|
||||
text: 'Torrent Inspector',
|
||||
},
|
||||
'show-labels-dialog': {
|
||||
enabled: false,
|
||||
text: 'Edit Labels…',
|
||||
},
|
||||
'show-move-dialog': {
|
||||
enabled: false,
|
||||
shortcut: 'Alt+L',
|
||||
|
@ -185,9 +189,10 @@ export class ActionManager extends EventTarget {
|
|||
'move-down',
|
||||
'move-top',
|
||||
'move-up',
|
||||
'show-inspector',
|
||||
'show-move-dialog',
|
||||
'remove-selected-torrents',
|
||||
'show-inspector',
|
||||
'show-labels-dialog',
|
||||
'show-move-dialog',
|
||||
'trash-selected-torrents',
|
||||
'verify-selected-torrents',
|
||||
]);
|
||||
|
|
|
@ -89,6 +89,7 @@ export class ContextMenu extends EventTarget {
|
|||
add_item('verify-selected-torrents');
|
||||
add_item('show-move-dialog');
|
||||
add_item('show-rename-dialog');
|
||||
add_item('show-labels-dialog');
|
||||
add_separator();
|
||||
add_item('reannounce-selected-torrents');
|
||||
add_separator();
|
||||
|
|
|
@ -117,6 +117,7 @@ export class Inspector extends EventTarget {
|
|||
['origin', 'Origin:'],
|
||||
['magnetLink', 'Magnet:'],
|
||||
['comment', 'Comment:'],
|
||||
['labels', 'Labels:'],
|
||||
];
|
||||
for (const [name, text] of rows) {
|
||||
elements[name] = append_row(text);
|
||||
|
@ -490,6 +491,10 @@ export class Inspector extends EventTarget {
|
|||
setTextContent(e.info.comment, string);
|
||||
}
|
||||
|
||||
// labels
|
||||
string = torrents.length === 0 ? none : torrents[0].getLabels().join(', ');
|
||||
setTextContent(e.info.labels, string);
|
||||
|
||||
// origin
|
||||
if (torrents.length === 0) {
|
||||
string = none;
|
||||
|
|
88
web/src/labels-dialog.js
Normal file
88
web/src/labels-dialog.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/* @license This file Copyright (C) 2020-2022 Mnemosyne LLC.
|
||||
It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
||||
or any future license endorsed by Mnemosyne LLC.
|
||||
License text can be found in the licenses/ folder. */
|
||||
|
||||
import { createDialogContainer } from './utils.js';
|
||||
|
||||
export class LabelsDialog extends EventTarget {
|
||||
constructor(controller, remote) {
|
||||
super();
|
||||
|
||||
this.controller = controller;
|
||||
this.remote = remote;
|
||||
this.elements = {};
|
||||
this.torrents = [];
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
const torrents = this.controller.getSelectedTorrents();
|
||||
if (torrents.length === 0) {
|
||||
console.error('At least one selected torrent expected.');
|
||||
return;
|
||||
}
|
||||
const [first] = torrents;
|
||||
|
||||
this.torrents = torrents;
|
||||
this.elements = LabelsDialog._create();
|
||||
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||
this.elements.confirm.addEventListener('click', () => this._onConfirm());
|
||||
this.elements.entry.value = first.getLabels().join(', ');
|
||||
document.body.append(this.elements.root);
|
||||
|
||||
this.elements.entry.focus();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.elements.root.remove();
|
||||
this.dispatchEvent(new Event('close'));
|
||||
|
||||
delete this.elements;
|
||||
delete this.torrents;
|
||||
}
|
||||
|
||||
_onDismiss() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
_onConfirm() {
|
||||
const { torrents } = this;
|
||||
const { remote } = this;
|
||||
const ids = torrents.map((t) => t.getId());
|
||||
const { elements } = this;
|
||||
const { entry } = elements;
|
||||
const { value } = entry;
|
||||
const labels = value.split(/ *, */).filter((l) => l.length > 0);
|
||||
|
||||
remote.setLabels(ids, labels, (response) => {
|
||||
if (response.result === 'success') {
|
||||
for (const t of torrents) {
|
||||
t.refresh({ labels });
|
||||
}
|
||||
}
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
static _create() {
|
||||
const elements = createDialogContainer('labels-dialog');
|
||||
elements.root.setAttribute('aria-label', 'Edit Labels');
|
||||
elements.heading.textContent = 'Edit Labels:';
|
||||
elements.confirm.textContent = 'Save';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', 'torrent-labels');
|
||||
label.textContent = 'Labels:';
|
||||
elements.workarea.append(label);
|
||||
|
||||
const entry = document.createElement('input');
|
||||
entry.setAttribute('type', 'text');
|
||||
entry.id = 'torrent-labels';
|
||||
elements.entry = entry;
|
||||
elements.workarea.append(entry);
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
|
@ -103,6 +103,14 @@ export class Remote {
|
|||
this.sendRequest(o, callback, context);
|
||||
}
|
||||
|
||||
setLabels(torrentIds, labels, callback) {
|
||||
const args = {
|
||||
ids: torrentIds,
|
||||
labels,
|
||||
};
|
||||
this.sendRequest({ arguments: args, method: 'torrent-set' }, callback);
|
||||
}
|
||||
|
||||
loadDaemonStats(callback, context) {
|
||||
const o = {
|
||||
method: 'session-stats',
|
||||
|
|
|
@ -18,6 +18,12 @@ const TorrentRendererHelper = {
|
|||
}
|
||||
return `ETA: ${Formatter.timeInterval(eta)}`;
|
||||
},
|
||||
formatLabels: (t) => {
|
||||
if (t.getLabels().length > 0) {
|
||||
return `🏷 ${t.getLabels().join(', ')}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
formatUL: (t) => {
|
||||
return `▲${Formatter.speedBps(t.getUploadSpeed())}`;
|
||||
},
|
||||
|
@ -215,6 +221,10 @@ export class TorrentRendererFull {
|
|||
setTextContent(e, t.getName());
|
||||
e.classList.toggle('paused', is_stopped);
|
||||
|
||||
// labels
|
||||
e = root._labels_container;
|
||||
setTextContent(e, TorrentRendererHelper.formatLabels(t));
|
||||
|
||||
// progressbar
|
||||
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
|
||||
root._progressbar.classList.add('full');
|
||||
|
@ -251,6 +261,9 @@ export class TorrentRendererFull {
|
|||
const name = document.createElement('div');
|
||||
name.className = 'torrent-name';
|
||||
|
||||
const labels = document.createElement('div');
|
||||
labels.className = 'torrent-labels';
|
||||
|
||||
const peers = document.createElement('div');
|
||||
peers.className = 'torrent-peer-details';
|
||||
|
||||
|
@ -268,12 +281,14 @@ export class TorrentRendererFull {
|
|||
|
||||
root.append(icon);
|
||||
root.append(name);
|
||||
root.append(labels);
|
||||
root.append(peers);
|
||||
root.append(progress);
|
||||
root.append(details);
|
||||
|
||||
root._icon = icon;
|
||||
root._name_container = name;
|
||||
root._labels_container = labels;
|
||||
root._peer_details_container = peers;
|
||||
root._progress_details_container = details;
|
||||
root._progressbar = progressbar;
|
||||
|
@ -323,6 +338,10 @@ export class TorrentRendererCompact {
|
|||
e.classList.toggle('paused', t.isStopped());
|
||||
setTextContent(e, t.getName());
|
||||
|
||||
// labels
|
||||
e = root._labels_container;
|
||||
setTextContent(e, TorrentRendererHelper.formatLabels(t));
|
||||
|
||||
// peer details
|
||||
const has_error = t.getError() !== Torrent._ErrNone;
|
||||
e = root._details_container;
|
||||
|
@ -350,17 +369,22 @@ export class TorrentRendererCompact {
|
|||
const details = document.createElement('div');
|
||||
details.className = 'torrent-peer-details compact';
|
||||
|
||||
const labels = document.createElement('div');
|
||||
labels.className = 'torrent-labels compact';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'torrent-name compact';
|
||||
|
||||
const root = document.createElement('li');
|
||||
root.append(progressbar);
|
||||
root.append(details);
|
||||
root.append(labels);
|
||||
root.append(name);
|
||||
root.append(icon);
|
||||
root.className = 'torrent compact';
|
||||
root._progressbar = progressbar;
|
||||
root._details_container = details;
|
||||
root._labels_container = labels;
|
||||
root._name_container = name;
|
||||
return root;
|
||||
}
|
||||
|
|
|
@ -203,6 +203,9 @@ export class Torrent extends EventTarget {
|
|||
getId() {
|
||||
return this.fields.id;
|
||||
}
|
||||
getLabels() {
|
||||
return this.fields.labels.sort();
|
||||
}
|
||||
getLastActivity() {
|
||||
return this.fields.activityDate;
|
||||
}
|
||||
|
@ -419,19 +422,36 @@ export class Torrent extends EventTarget {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param filter one of Prefs.Filter*
|
||||
* @param state one of Prefs.Filter*
|
||||
* @param tracker tracker name
|
||||
* @param search substring to look for, or null
|
||||
* @param labels array of labels. Empty array matches all.
|
||||
* @return true if it passes the test, false if it fails
|
||||
*/
|
||||
test(state, search, tracker) {
|
||||
test(state, tracker, search, labels) {
|
||||
// flter by state...
|
||||
let pass = this.testState(state);
|
||||
|
||||
// maybe filter by text...
|
||||
if (pass && search && search.length > 0) {
|
||||
if (pass && search) {
|
||||
pass = this.getCollatedName().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
// maybe filter by labels...
|
||||
if (pass) {
|
||||
for (const l of labels) {
|
||||
let m = false;
|
||||
for (let j = 0; j < this.getLabels().length; j++) {
|
||||
if (l === this.getLabels()[j]) {
|
||||
m = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pass = pass && m;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe filter by tracker...
|
||||
if (pass && tracker && tracker.length > 0) {
|
||||
pass = this.getCollatedTrackers().includes(tracker);
|
||||
|
@ -627,6 +647,7 @@ Torrent.Fields.Stats = [
|
|||
'eta',
|
||||
'isFinished',
|
||||
'isStalled',
|
||||
'labels',
|
||||
'leftUntilDone',
|
||||
'metadataPercentComplete',
|
||||
'peersConnected',
|
||||
|
|
|
@ -14,6 +14,7 @@ import { PrefsDialog } from './prefs-dialog.js';
|
|||
import { Remote, RPC } from './remote.js';
|
||||
import { RemoveDialog } from './remove-dialog.js';
|
||||
import { RenameDialog } from './rename-dialog.js';
|
||||
import { LabelsDialog } from './labels-dialog.js';
|
||||
import { ShortcutsDialog } from './shortcuts-dialog.js';
|
||||
import { StatisticsDialog } from './statistics-dialog.js';
|
||||
import { Torrent } from './torrent.js';
|
||||
|
@ -170,6 +171,9 @@ export class Transmission extends EventTarget {
|
|||
case 'show-rename-dialog':
|
||||
this.setCurrentPopup(new RenameDialog(this, this.remote));
|
||||
break;
|
||||
case 'show-labels-dialog':
|
||||
this.setCurrentPopup(new LabelsDialog(this, this.remote));
|
||||
break;
|
||||
case 'start-all-torrents':
|
||||
this._startTorrents(this._getAllTorrents());
|
||||
break;
|
||||
|
@ -909,11 +913,21 @@ TODO: fix this when notifications get fixed
|
|||
|
||||
_refilter(rebuildEverything) {
|
||||
const { sort_mode, sort_direction, filter_mode } = this.prefs;
|
||||
const filter_text = this.filterText;
|
||||
const filter_tracker = this.filterTracker;
|
||||
const renderer = this.torrentRenderer;
|
||||
const list = this.elements.torrent_list;
|
||||
|
||||
let filter_text = null;
|
||||
let labels = null;
|
||||
const m = /^labels:([\w,]*)(.*)$/.exec(this.filterText);
|
||||
if (m) {
|
||||
filter_text = m[2].trim();
|
||||
labels = m[1].split(',');
|
||||
} else {
|
||||
filter_text = this.filterText;
|
||||
labels = [];
|
||||
}
|
||||
|
||||
const countRows = () => [...list.children].length;
|
||||
const countSelectedRows = () =>
|
||||
[...list.children].reduce(
|
||||
|
@ -958,7 +972,7 @@ TODO: fix this when notifications get fixed
|
|||
for (const row of dirty_rows) {
|
||||
const id = row.getTorrentId();
|
||||
const t = this._torrents[id];
|
||||
if (t && t.test(filter_mode, filter_text, filter_tracker)) {
|
||||
if (t && t.test(filter_mode, filter_tracker, filter_text, labels)) {
|
||||
temporary.push(row);
|
||||
}
|
||||
this.dirtyTorrents.delete(id);
|
||||
|
@ -969,7 +983,7 @@ TODO: fix this when notifications get fixed
|
|||
// but don't already have a row
|
||||
for (const id of this.dirtyTorrents.values()) {
|
||||
const t = this._torrents[id];
|
||||
if (t && t.test(filter_mode, filter_text, filter_tracker)) {
|
||||
if (t && t.test(filter_mode, filter_tracker, filter_text, labels)) {
|
||||
const row = new TorrentRow(renderer, this, t);
|
||||
const e = row.getElement();
|
||||
e.row = row;
|
||||
|
|
|
@ -450,11 +450,11 @@ $popup-top: 61px; // TODO: ugly that this is hardcoded
|
|||
display: grid;
|
||||
grid-column-gap: 12px;
|
||||
grid-template-areas:
|
||||
'icon name'
|
||||
'icon peers'
|
||||
'icon progressbar'
|
||||
'icon progress-text';
|
||||
grid-template-columns: $icon-size 1fr;
|
||||
'icon name labels'
|
||||
'icon peers peers'
|
||||
'icon progressbar progressbar'
|
||||
'icon progress-text progress-text';
|
||||
grid-template-columns: $icon-size auto 1fr;
|
||||
padding: 2px 12px;
|
||||
|
||||
.icon {
|
||||
|
@ -468,6 +468,10 @@ $popup-top: 61px; // TODO: ugly that this is hardcoded
|
|||
grid-area: name;
|
||||
}
|
||||
|
||||
.torrent-labels {
|
||||
grid-area: labels;
|
||||
}
|
||||
|
||||
.torrent-peer-details {
|
||||
grid-area: peers;
|
||||
}
|
||||
|
@ -539,7 +543,6 @@ $popup-top: 61px; // TODO: ugly that this is hardcoded
|
|||
white-space: nowrap;
|
||||
|
||||
&.compact {
|
||||
flex: 1;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@ -549,6 +552,21 @@ $popup-top: 61px; // TODO: ugly that this is hardcoded
|
|||
}
|
||||
}
|
||||
|
||||
.torrent-labels {
|
||||
font-size: x-small;
|
||||
font-weight: normal;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.compact {
|
||||
flex: 1;
|
||||
font-size: x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.torrent-progress-details,
|
||||
.torrent-peer-details {
|
||||
font-size: x-small;
|
||||
|
|
Loading…
Add table
Reference in a new issue