1
0
Fork 0
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:
Viacheslav Chimishuk 2022-02-10 17:25:11 +02:00 committed by GitHub
parent 663fba71c2
commit 0973cfd96d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 199 additions and 15 deletions

File diff suppressed because one or more lines are too long

View file

@ -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',
]);

View file

@ -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();

View file

@ -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
View 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;
}
}

View file

@ -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',

View file

@ -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;
}

View file

@ -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',

View file

@ -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;

View file

@ -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;