mirror of
https://github.com/transmission/transmission.git
synced 2025-12-20 10:28:32 +00:00
feat: web client refresh (#1476)
Give the web client a major overhaul. User-visible highlights include: * Mobile is now fully supported. * Added fullscreen support on mobile. * Better support for dark mode. * Added mime icons to the torrent list. * Improved theme consistency across the app. Maintainer highlights include: * Updated code to use ES6 APIs. * No longer uses jQuery UI. * No longer uses jQuery. * Use Webpack to bundle the Javascript, CSS, and assets together -- the entire bundle size is now 68K gzipped. * Added eslint / prettier / stylelint tooling. * Uses torrent-get's 'table' mode for more efficient RPC calls.
This commit is contained in:
466
web/src/overflow-menu.js
Normal file
466
web/src/overflow-menu.js
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||
*
|
||||
* It may be used under the GNU GPL versions 2 or 3
|
||||
* or any future license endorsed by Mnemosyne LLC.
|
||||
*/
|
||||
|
||||
import { Formatter } from './formatter.js';
|
||||
import { Prefs } from './prefs.js';
|
||||
import { RPC } from './remote.js';
|
||||
import { OutsideClickListener, setEnabled } from './utils.js';
|
||||
|
||||
export class OverflowMenu extends EventTarget {
|
||||
constructor(session_manager, prefs, remote, action_manager) {
|
||||
super();
|
||||
|
||||
this.action_listener = this._onActionChange.bind(this);
|
||||
this.action_manager = action_manager;
|
||||
this.action_manager.addEventListener('change', this.action_listener);
|
||||
|
||||
this.prefs_listener = this._onPrefsChange.bind(this);
|
||||
this.prefs = prefs;
|
||||
this.prefs.addEventListener('change', this.prefs_listener);
|
||||
|
||||
this.closed = false;
|
||||
this.remote = remote;
|
||||
this.name = 'overflow-menu';
|
||||
|
||||
this.session_listener = this._onSessionChange.bind(this);
|
||||
this.session_manager = session_manager;
|
||||
this.session_manager.addEventListener(
|
||||
'session-change',
|
||||
this.session_listener
|
||||
);
|
||||
|
||||
const { session_properties } = session_manager;
|
||||
Object.assign(this, this._create(session_properties));
|
||||
|
||||
this.outside = new OutsideClickListener(this.root);
|
||||
this.outside.addEventListener('click', () => this.close());
|
||||
Object.seal(this);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
document.body.append(this.root);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.closed) {
|
||||
this.outside.stop();
|
||||
this.session_manager.removeEventListener(
|
||||
'session-change',
|
||||
this.session_listener
|
||||
);
|
||||
this.action_manager.removeEventListener('change', this.action_listener);
|
||||
this.prefs.removeEventListener('change', this.prefs_listener);
|
||||
|
||||
this.root.remove();
|
||||
this.dispatchEvent(new Event('close'));
|
||||
|
||||
for (const key of Object.keys(this)) {
|
||||
this[key] = null;
|
||||
}
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onSessionChange(event_) {
|
||||
const { alt_speed_check } = this.elements;
|
||||
const { session_properties } = event_;
|
||||
alt_speed_check.checked = session_properties[RPC._TurtleState];
|
||||
}
|
||||
|
||||
_onPrefsChange(event_) {
|
||||
switch (event_.key) {
|
||||
case Prefs.SortDirection:
|
||||
case Prefs.SortMode:
|
||||
this.root.querySelector(`[data-pref="${event_.key}"]`).value =
|
||||
event_.value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onActionChange(event_) {
|
||||
const element = this.actions[event_.action];
|
||||
if (element) {
|
||||
this._updateElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
_updateElement(element) {
|
||||
if (element.dataset.action) {
|
||||
const { action } = element.dataset;
|
||||
const shortcuts = this.action_manager.keyshortcuts(action);
|
||||
if (shortcuts) {
|
||||
element.setAttribute('aria-keyshortcuts', shortcuts);
|
||||
}
|
||||
setEnabled(element, this.action_manager.isEnabled(action));
|
||||
}
|
||||
}
|
||||
|
||||
_onClick(event_) {
|
||||
const { action, pref } = event_.target.dataset;
|
||||
|
||||
if (action) {
|
||||
this.action_manager.click(action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pref) {
|
||||
this.prefs[pref] = event_.target.value;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('unhandled');
|
||||
console.log(event_);
|
||||
console.trace();
|
||||
}
|
||||
|
||||
_create(session_properties) {
|
||||
const actions = {};
|
||||
const on_click = this._onClick.bind(this);
|
||||
const elements = {};
|
||||
|
||||
const make_section = (classname, title) => {
|
||||
const section = document.createElement('fieldset');
|
||||
section.classList.add('section', classname);
|
||||
const legend = document.createElement('legend');
|
||||
legend.classList.add('title');
|
||||
legend.textContent = title;
|
||||
section.append(legend);
|
||||
return section;
|
||||
};
|
||||
|
||||
const make_button = (parent, text, action) => {
|
||||
const e = document.createElement('button');
|
||||
e.textContent = text;
|
||||
e.addEventListener('click', on_click);
|
||||
parent.append(e);
|
||||
if (action) {
|
||||
e.dataset.action = action;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.classList.add('overflow-menu', 'popup');
|
||||
|
||||
let section = make_section('display', 'Display');
|
||||
root.append(section);
|
||||
|
||||
let options = document.createElement('div');
|
||||
options.id = 'display-options';
|
||||
section.append(options);
|
||||
|
||||
// sort mode
|
||||
|
||||
let div = document.createElement('div');
|
||||
options.append(div);
|
||||
|
||||
let label = document.createElement('label');
|
||||
label.id = 'display-sort-mode-label';
|
||||
label.textContent = 'Sort by';
|
||||
div.append(label);
|
||||
|
||||
let select = document.createElement('select');
|
||||
select.id = 'display-sort-mode-select';
|
||||
select.dataset.pref = Prefs.SortMode;
|
||||
div.append(select);
|
||||
|
||||
const sort_modes = [
|
||||
[Prefs.SortByActivity, 'Activity'],
|
||||
[Prefs.SortByAge, 'Age'],
|
||||
[Prefs.SortByName, 'Name'],
|
||||
[Prefs.SortByProgress, 'Progress'],
|
||||
[Prefs.SortByQueue, 'Queue order'],
|
||||
[Prefs.SortByRatio, 'Ratio'],
|
||||
[Prefs.SortBySize, 'Size'],
|
||||
[Prefs.SortByState, 'State'],
|
||||
];
|
||||
for (const [value, text] of sort_modes) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = text;
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
label.setAttribute('for', select.id);
|
||||
select.value = this.prefs.sort_mode;
|
||||
select.addEventListener('change', (event_) => {
|
||||
this.prefs.sort_mode = event_.target.value;
|
||||
});
|
||||
|
||||
// sort direction
|
||||
|
||||
div = document.createElement('div');
|
||||
options.append(div);
|
||||
|
||||
let check = document.createElement('input');
|
||||
check.id = 'display-sort-reverse-check';
|
||||
check.dataset.pref = Prefs.SortDirection;
|
||||
check.type = 'checkbox';
|
||||
div.append(check);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'display-sort-reverse-label';
|
||||
label.setAttribute('for', check.id);
|
||||
label.textContent = 'Reverse sort';
|
||||
div.append(label);
|
||||
|
||||
check.checked = this.prefs.sort_direction !== Prefs.SortAscending;
|
||||
check.addEventListener('input', (event_) => {
|
||||
this.prefs.sort_direction = event_.target.checked
|
||||
? Prefs.SortDescending
|
||||
: Prefs.SortAscending;
|
||||
});
|
||||
|
||||
// compact
|
||||
|
||||
div = document.createElement('div');
|
||||
options.append(div);
|
||||
|
||||
const action = 'toggle-compact-rows';
|
||||
check = document.createElement('input');
|
||||
check.id = 'display-compact-check';
|
||||
check.dataset.action = action;
|
||||
check.type = 'checkbox';
|
||||
div.append(check);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'display-compact-label';
|
||||
label.for = check.id;
|
||||
label.setAttribute('for', check.id);
|
||||
label.textContent = this.action_manager.text(action);
|
||||
div.append(label);
|
||||
|
||||
check.checked = this.prefs.display_mode === Prefs.DisplayCompact;
|
||||
check.addEventListener('input', (event_) => {
|
||||
const { checked } = event_.target;
|
||||
this.prefs.display_mode = checked
|
||||
? Prefs.DisplayCompact
|
||||
: Prefs.DisplayFull;
|
||||
});
|
||||
|
||||
// fullscreen
|
||||
|
||||
div = document.createElement('div');
|
||||
options.append(div);
|
||||
|
||||
check = document.createElement('input');
|
||||
check.id = 'display-fullscreen-check';
|
||||
check.type = 'checkbox';
|
||||
const is_fullscreen = () => document.fullscreenElement !== null;
|
||||
check.checked = is_fullscreen();
|
||||
check.addEventListener('input', () => {
|
||||
if (is_fullscreen()) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
document.body.requestFullscreen();
|
||||
}
|
||||
});
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
check.checked = is_fullscreen();
|
||||
});
|
||||
div.append(check);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'display-fullscreen-label';
|
||||
label.for = check.id;
|
||||
label.setAttribute('for', check.id);
|
||||
label.textContent = 'Fullscreen';
|
||||
div.append(label);
|
||||
|
||||
section = make_section('speed', 'Speed Limit');
|
||||
root.append(section);
|
||||
|
||||
options = document.createElement('div');
|
||||
options.id = 'speed-options';
|
||||
section.append(options);
|
||||
|
||||
// speed up
|
||||
|
||||
div = document.createElement('div');
|
||||
div.classList.add('speed-up');
|
||||
options.append(div);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'speed-up-label';
|
||||
label.textContent = 'Upload:';
|
||||
div.append(label);
|
||||
|
||||
const unlimited = 'Unlimited';
|
||||
select = document.createElement('select');
|
||||
select.id = 'speed-up-select';
|
||||
div.append(select);
|
||||
|
||||
const speeds = ['10', '100', '200', '500', '750', unlimited];
|
||||
for (const speed of [
|
||||
...new Set(speeds)
|
||||
.add(`${session_properties[RPC._UpSpeedLimit]}`)
|
||||
.values(),
|
||||
].sort()) {
|
||||
const option = document.createElement('option');
|
||||
option.value = speed;
|
||||
option.textContent =
|
||||
speed === unlimited ? unlimited : Formatter.speed(speed);
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
label.setAttribute('for', select.id);
|
||||
select.value = session_properties[RPC._UpSpeedLimited]
|
||||
? `${session_properties[RPC._UpSpeedLimit]}`
|
||||
: unlimited;
|
||||
select.addEventListener('change', (event_) => {
|
||||
const { value } = event_.target;
|
||||
console.log(event_);
|
||||
if (event_.target.value === unlimited) {
|
||||
this.remote.savePrefs({ [RPC._UpSpeedLimited]: false });
|
||||
} else {
|
||||
this.remote.savePrefs({
|
||||
[RPC._UpSpeedLimited]: true,
|
||||
[RPC._UpSpeedLimit]: Number.parseInt(value, 10),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// speed down
|
||||
|
||||
div = document.createElement('div');
|
||||
div.classList.add('speed-down');
|
||||
options.append(div);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'speed-down-label';
|
||||
label.textContent = 'Download:';
|
||||
div.append(label);
|
||||
|
||||
select = document.createElement('select');
|
||||
select.id = 'speed-down-select';
|
||||
div.append(select);
|
||||
|
||||
for (const speed of [
|
||||
...new Set(speeds)
|
||||
.add(`${session_properties[RPC._DownSpeedLimit]}`)
|
||||
.values(),
|
||||
].sort()) {
|
||||
const option = document.createElement('option');
|
||||
option.value = speed;
|
||||
option.textContent = speed;
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
label.setAttribute('for', select.id);
|
||||
select.value = session_properties[RPC._DownSpeedLimited]
|
||||
? `${session_properties[RPC._DownSpeedLimit]}`
|
||||
: unlimited;
|
||||
select.addEventListener('change', (event_) => {
|
||||
const { value } = event_.target;
|
||||
console.log(event_);
|
||||
if (event_.target.value === unlimited) {
|
||||
this.remote.savePrefs({ [RPC._DownSpeedLimited]: false });
|
||||
} else {
|
||||
this.remote.savePrefs({
|
||||
[RPC._DownSpeedLimited]: true,
|
||||
[RPC._DownSpeedLimit]: Number.parseInt(value, 10),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// alt speed
|
||||
|
||||
div = document.createElement('div');
|
||||
div.classList.add('alt-speed');
|
||||
options.append(div);
|
||||
|
||||
check = document.createElement('input');
|
||||
check.id = 'alt-speed-check';
|
||||
check.type = 'checkbox';
|
||||
check.checked = session_properties[RPC._TurtleState];
|
||||
check.addEventListener('change', (event_) => {
|
||||
this.remote.savePrefs({
|
||||
[RPC._TurtleState]: event_.target.checked,
|
||||
});
|
||||
});
|
||||
div.append(check);
|
||||
elements.alt_speed_check = check;
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'alt-speed-image';
|
||||
label.setAttribute('for', check.id);
|
||||
div.append(label);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'alt-speed-label';
|
||||
label.setAttribute('for', check.id);
|
||||
label.textContent = 'Use Temp limits';
|
||||
div.append(label);
|
||||
|
||||
label = document.createElement('label');
|
||||
label.id = 'alt-speed-values-label';
|
||||
label.setAttribute('for', check.id);
|
||||
|
||||
const up = Formatter.speed(session_properties[RPC._TurtleUpSpeedLimit]);
|
||||
const dn = Formatter.speed(session_properties[RPC._TurtleDownSpeedLimit]);
|
||||
label.textContent = `(${up} up, ${dn} down)`;
|
||||
div.append(label);
|
||||
|
||||
section = make_section('actions', 'Actions');
|
||||
root.append(section);
|
||||
|
||||
for (const action_name of [
|
||||
'show-preferences-dialog',
|
||||
'pause-all-torrents',
|
||||
'start-all-torrents',
|
||||
]) {
|
||||
const text = this.action_manager.text(action_name);
|
||||
actions[action_name] = make_button(section, text, action_name);
|
||||
}
|
||||
|
||||
section = make_section('info', 'Info');
|
||||
root.append(section);
|
||||
|
||||
options = document.createElement('div');
|
||||
section.append(options);
|
||||
|
||||
for (const action_name of [
|
||||
'show-about-dialog',
|
||||
'show-shortcuts-dialog',
|
||||
'show-statistics-dialog',
|
||||
]) {
|
||||
const text = this.action_manager.text(action_name);
|
||||
actions[action_name] = make_button(options, text, action_name);
|
||||
}
|
||||
|
||||
section = make_section('links', 'Links');
|
||||
root.append(section);
|
||||
|
||||
options = document.createElement('div');
|
||||
section.append(options);
|
||||
|
||||
let e = document.createElement('a');
|
||||
e.href = 'https://transmissionbt.com/';
|
||||
e.tabindex = '0';
|
||||
e.textContent = 'Homepage';
|
||||
options.append(e);
|
||||
|
||||
e = document.createElement('a');
|
||||
e.href = 'https://transmissionbt.com/donate/';
|
||||
e.tabindex = '0';
|
||||
e.textContent = 'Tip Jar';
|
||||
options.append(e);
|
||||
|
||||
e = document.createElement('a');
|
||||
e.href = 'https://github.com/transmission/transmission/';
|
||||
e.tabindex = '0';
|
||||
e.textContent = 'Source Code';
|
||||
options.append(e);
|
||||
|
||||
Object.values(actions).forEach(this._updateElement.bind(this));
|
||||
return { actions, elements, root };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user