mirror of
https://github.com/transmission/transmission.git
synced 2025-12-12 20:35:49 +01:00
* refactor: change `leftUntilDone` to `left_until_done` * refactor: change `magnetLink` to `magnet_link` * refactor: change `manualAnnounceTime` to `manual_announce_time` * refactor: change `maxConnectedPeers` to `max_connected_peers` * refactor: change `metadataPercentComplete` to `metadata_percent_complete` * refactor: change `peersConnected` to `peers_connected` * refactor: change `peersFrom` to `peers_from` * refactor: change `peersGettingFromUs` to `peers_getting_from_us` * refactor: change `peersSendingToUs` to `peers_sending_to_us` * refactor: change `percentComplete` to `percent_complete` * refactor: change `percentDone` to `percent_done` * refactor: change `pieceCount` to `piece_count` * refactor: use quark when possible * refactor: change `pieceSize` to `piece_size` * refactor: change `primary-mime-type` to `primary_mime_type` * refactor: change `rateDownload` to `rate_download` * refactor: change `rateUpload` to `rate_upload` * refactor: change `recheckProgress` to `recheck_progress` * refactor: change `secondsDownloading` to `seconds_downloading` * refactor: change `secondsSeeding` to `seconds_seeding` * refactor: change `sizeWhenDone` to `size_when_done` * refactor: change `startDate` to `start_date` * refactor: change `trackerStats` to `tracker_stats` * refactor: change `totalSize` to `total_size` * refactor: change `torrentFile` to `torrent_file` * refactor: change `uploadedEver` to `uploaded_ever` * refactor: change `uploadRatio` to `upload_ratio` * refactor: change `webseedsSendingToUs` to `webseeds_sending_to_us` * refactor: change `bytesCompleted` to `bytes_completed` * refactor: change `clientName` to `client_name` * refactor: change `clientIsChoked` to `client_is_choked` * refactor: change `clientIsInterested` to `client_is_interested` * refactor: change `flagStr` to `flag_str` * refactor: change `isDownloadingFrom` to `is_downloading_from` * refactor: change `isEncrypted` to `is_encrypted` * refactor: change `isIncoming` to `is_incoming` * refactor: change `isUploadingTo` to `is_uploading_to` * refactor: change `isUTP` to `is_utp` * refactor: change `peerIsChoked` to `peer_is_choked` * refactor: change `peerIsInterested` to `peer_is_interested` * refactor: change `rateToClient` to `rate_to_client` * refactor: change `rateToPeer` to `rate_to_peer` * refactor: change `fromCache` to `from_cache` * refactor: change `fromDht` to `from_dht` * refactor: change `fromIncoming` to `from_incoming` * refactor: change `fromLpd` to `from_lpd` * refactor: change `fromLtep` to `from_ltep` * refactor: change `fromPex` to `from_pex` * refactor: change `fromTracker` to `from_tracker` * refactor: change `announceState` to `announce_state` * refactor: change `downloadCount` to `download_count` * refactor: change `hasAnnounced` to `has_announced` * refactor: change `hasScraped` to `has_scraped` * refactor: change `isBackup` to `is_backup` * refactor: change `lastAnnouncePeerCount` to `last_announce_peer_count` * refactor: change `lastAnnounceResult` to `last_announce_result` * refactor: change `lastAnnounceStartTime` to `last_announce_start_time` * refactor: change `lastAnnounceSucceeded` to `last_announce_succeeded` * refactor: change `lastAnnounceTime` to `last_announce_time` * refactor: change `lastAnnounceTimedOut` to `last_announce_timed_out` * refactor: change `lastScrapeResult` to `last_scrape_result` * refactor: change `lastScrapeStartTime` to `last_scrape_start_time` * refactor: change `lastScrapeSucceeded` to `last_scrape_succeeded` * refactor: change `lastScrapeTime` to `last_scrape_time` * refactor: change `lastScrapeTimedOut` to `last_scrape_timed_out` * refactor: change `leecherCount` to `leecher_count` * refactor: change `nextAnnounceTime` to `next_announce_time` * refactor: change `nextScrapeTime` to `next_scrape_time` * refactor: change `scrapeState` to `scrape_state` * refactor: change `seederCount` to `seeder_count` * refactor: change `torrent-added` to `torrent_added` * refactor: change `torrent-duplicate` to `torrent_duplicate` * refactor: change `torrent-remove` to `torrent_remove` * refactor: change `delete-local-data` to `delete_local_data` * refactor: change `torrent-rename-path` to `torrent_rename_path` * refactor: change `alt-speed-down` to `alt_speed_down` * refactor: convert `pref_toggle_entries` to quark array * refactor: change `alt-speed-enabled` to `alt_speed_enabled` * refactor: change `compact-view` to `compact_view` * refactor: change `sort-reversed` to `sort_reversed` * refactor: change `show-filterbar` to `show_filterbar` * refactor: change `show-statusbar` to `show_statusbar` * refactor: change `show-toolbar` to `show_toolbar` * refactor: change `alt-speed-time-begin` to `alt_speed_time_begin` * refactor: change `alt-speed-time-day` to `alt_speed_time_day` * refactor: change `alt-speed-time-end` to `alt_speed_time_end` * refactor: change `alt-speed-up` to `alt_speed_up` * refactor: change `alt-speed-time-enabled` to `alt_speed_time_enabled` * refactor: change `blocklist-enabled` to `blocklist_enabled` * refactor: change `blocklist-size` to `blocklist_size` * refactor: change `blocklist-url` to `blocklist_url` * refactor: change `cache-size-mb` to `cache_size_mb` * refactor: change `config-dir` to `config_dir` * refactor: change `default-trackers` to `default_trackers` * refactor: change `dht-enabled` to `dht_enabled` * refactor: change `download-dir-free-space` to `download_dir_free_space` * refactor: change `download-queue-enabled` to `download_queue_enabled` * refactor: change `download-queue-size` to `download_queue_size` * refactor: change `idle-seeding-limit-enabled` to `idle_seeding_limit_enabled` * refactor: change `idle-seeding-limit` to `idle_seeding_limit` * refactor: change `incomplete-dir-enabled` to `incomplete_dir_enabled` * refactor: change `incomplete-dir` to `incomplete_dir` * refactor: change `lpd-enabled` to `lpd_enabled` * refactor: change `peer-limit-global` to `peer_limit_global` * refactor: change `peer-limit-per-torrent` to `peer_limit_per_torrent` * refactor: change `peer-port-random-on-start` to `peer_port_random_on_start` * refactor: change `peer-port` to `peer_port` * refactor: change `pex-enabled` to `pex_enabled` * refactor: change `port-forwarding-enabled` to `port_forwarding_enabled` * refactor: change `queue-stalled-enabled` to `queue_stalled_enabled` * refactor: change `queue-stalled-minutes` to `queue_stalled_minutes` * refactor: change `rename-partial-files` to `rename_partial_files` * refactor: change `rpc-version-minimum` to `rpc_version_minimum` * refactor: change `rpc-version-semver` to `rpc_version_semver` * refactor: change `rpc-version` to `rpc_version` * refactor: change `script-torrent-added-enabled` to `script_torrent_added_enabled` * refactor: change `script-torrent-added-filename` to `script_torrent_added_filename` * refactor: change `script-torrent-done-enabled` to `script_torrent_done_enabled` * refactor: change `script-torrent-done-filename` to `script_torrent_done_filename` * refactor: change `script-torrent-done-seeding-enabled` to `script_torrent_done_seeding_enabled` * refactor: change `script-torrent-done-seeding-filename` to `script_torrent_done_seeding_filename` * refactor: change `seed-queue-enabled` to `seed_queue_enabled` * refactor: change `seed-queue-size` to `seed_queue_size` * refactor: change `seedRatioLimited` to `seed_ratio_limited` * refactor: change `session-id` to `session_id` * refactor: change `speed-limit-down-enabled` to `speed_limit_down_enabled` * refactor: change `speed-limit-down` to `speed_limit_down` * refactor: change `speed-limit-up-enabled` to `speed_limit_up_enabled` * refactor: change `speed-limit-up` to `speed_limit_up` * refactor: change `start-added-torrents` to `start_added_torrents` * refactor: change `trash-original-torrent-files` to `trash_original_torrent_files` * refactor: change `utp-enabled` to `utp_enabled` * refactor: change `tcp-enabled` to `tcp_enabled` * docs: add missing docs for RPC `tcp_enabled` * refactor: change `speed-units` to `speed_units` * refactor: change `speed-bytes` to `speed_bytes` * refactor: change `size-units` to `size_units` * refactor: change `size-bytes` to `size_bytes` * refactor: change `memory-units` to `memory_units` * refactor: change `memory-bytes` to `memory_bytes` * refactor: change `session-set` to `session_set` * refactor: change `session-get` to `session_get` * refactor: change `session-stats` to `session_stats` * refactor: change `activeTorrentCount` to `active_torrent_count` * refactor: change `downloadSpeed` to `download_speed` * refactor: change `pausedTorrentCount` to `paused_torrent_count` * refactor: change `torrentCount` to `torrent_count` * refactor: change `uploadSpeed` to `upload_speed` * refactor: change `cumulative-stats` to `cumulative_stats` * refactor: change `current-stats` to `current_stats` * refactor: change `uploadedBytes` and `uploaded-bytes` to `uploaded_bytes` * refactor: change `downloadedBytes` and `downloaded-bytes` to `downloaded_bytes` * refactor: change `filesAdded` and `files-added` to `files_added` * refactor: change `sessionCount` and `session-count` to `session_count` * refactor: change `secondsActive` and `seconds-active` to `seconds_active` * refactor: change `blocklist-update` to `blocklist_update` * refactor: change `port-test` to `port_test` * refactor: change `session-close` to `session_close` * refactor: change `queue-move-top` to `queue_move_top` * refactor: change `queue-move-up` to `queue_move_up` * refactor: change `queue-move-down` to `queue_move_down` * refactor: change `queue-move-bottom` to `queue_move_bottom` * refactor: change `free-space` to `free_space` * refactor: change `group-set` to `group_set` * refactor: change `group-get` to `group_get` * refactor: change `announce-ip` to `announce_ip` * refactor: change `announce-ip-enabled` to `announce_ip_enabled` * refactor: change `upload-slots-per-torrent` to `upload_slots_per_torrent` * refactor: change `trash-can-enabled` to `trash_can_enabled` * refactor: change `watch-dir-enabled` to `watch_dir_enabled` * refactor: change `watch-dir-force-generic` to `watch_dir_force_generic` * refactor: change `watch-dir` to `watch_dir` * refactor: change `message-level` to `message_level` * refactor: change `scrape-paused-torrents-enabled` to `scrape_paused_torrents_enabled` * refactor: change `torrent-added-verify-mode` to `torrent_added_verify_mode` * refactor: change `sleep-per-seconds-during-verify` to `sleep_per_seconds_during_verify` * refactor: change `bind-address-ipv4` to `bind_address_ipv4` * refactor: change `bind-address-ipv6` to `bind_address_ipv6` * refactor: change `peer-congestion-algorithm` to `peer_congestion_algorithm` * refactor: change `peer-socket-tos` to `peer_socket_tos` * refactor: change `peer-port-random-high` to `peer_port_random_high` * refactor: change `peer-port-random-low` to `peer_port_random_low` * refactor: change `anti-brute-force-enabled` to `anti_brute_force_enabled` * refactor: change `rpc-authentication-required` to `rpc_authentication_required` * refactor: change `rpc-bind-address` to `rpc_bind_address` * refactor: change `rpc-enabled` to `rpc_enabled` * refactor: change `rpc-host-whitelist` to `rpc_host_whitelist` * refactor: change `rpc-host-whitelist-enabled` to `rpc_host_whitelist_enabled` * refactor: change `rpc-password` to `rpc_password` * refactor: change `rpc-port` to `rpc_port` * refactor: change `rpc-socket-mode` to `rpc_socket_mode` * refactor: change `rpc-url` to `rpc_url` * refactor: change `rpc-username` to `rpc_username` * refactor: change `rpc-whitelist` to `rpc_whitelist` * refactor: change `rpc-whitelist-enabled` to `rpc_whitelist_enabled` * refactor: change `ratio-limit-enabled` to `ratio_limit_enabled` * refactor: change `ratio-limit` to `ratio_limit` * refactor: change `show-options-window` to `show_options_window` * refactor: change `open-dialog-dir` to `open_dialog_dir` * refactor: change `inhibit-desktop-hibernation` to `inhibit_desktop_hibernation` * refactor: change `show-notification-area-icon` to `show_notification_area_icon` * refactor: change `start-minimized` to `start_minimized` * refactor: change `torrent-added-notification-enabled` to `torrent_added_notification_enabled` * refactor: change `anti-brute-force-threshold` to `anti_brute_force_threshold` * refactor: change `torrent-complete-notification-enabled` to `torrent_complete_notification_enabled` * refactor: change `prompt-before-exit` to `prompt_before_exit` * refactor: change `sort-mode` to `sort_mode` * refactor: change `statusbar-stats` to `statusbar_stats` * refactor: change `show-extra-peer-details` to `show_extra_peer_details` * refactor: change `show-backup-trackers` to `show_backup_trackers` * refactor: change `blocklist-date` to `blocklist_date` * refactor: change `blocklist-updates-enabled` to `blocklist_updates_enabled` * refactor: change `main-window-layout-order` to `main_window_layout_order` * refactor: change `main-window-height` to `main_window_height` * refactor: change `main-window-width` to `main_window_width` * refactor: change `main-window-x` to `main_window_x` * refactor: change `main-window-y` to `main_window_y` * refactor: change `filter-mode` to `filter_mode` * refactor: change `filter-trackers` to `filter_trackers` * refactor: change `filter-text` to `filter_text` * refactor: change `remote-session-enabled` to `remote_session_enabled` * refactor: change `remote-session-host` to `remote_session_host` * refactor: change `remote-session-https` to `remote_session_https` * refactor: change `remote-session-password` to `remote_session_password` * refactor: change `remote-session-port` to `remote_session_port` * refactor: change `remote-session-requres-authentication` to `remote_session_requires_authentication` * refactor: change `remote-session-username` to `remote_session_username` * refactor: change `torrent-complete-sound-command` to `torrent_complete_sound_command` * refactor: change `torrent-complete-sound-enabled` to `torrent_complete_sound_enabled` * refactor: change `user-has-given-informed-consent` to `user_has_given_informed_consent` * refactor: change `read-clipboard` to `read_clipboard` * refactor: change `details-window-height` to `details_window_height` * refactor: change `details-window-width` to `details_window_width` * refactor: change `main-window-is-maximized` to `main_window_is_maximized` * refactor: change `port-is-open` to `port_is_open` * refactor: change `show-tracker-scrapes` to `show_tracker_scrapes` * refactor: change `max-peers` to `max_peers` * refactor: change `peers2-6` to `peers2_6` * refactor: change `seeding-time-seconds` to `seeding_time_seconds` * refactor: change `downloading-time-seconds` to `downloading_time_seconds` * refactor: change `ratio-mode` to `ratio_mode` * refactor: change `idle-limit` to `idle_limit` * refactor: change `idle-mode` to `idle_mode` * refactor: change `speed-Bps` to `speed_Bps` * refactor: change `use-global-speed-limit` to `use_global_speed_limit` * refactor: change `use-speed-limit` to `use_speed_limit` * chore: remove TODO comment * docs: add upgrade instructions to `5.0.0` * chore: bump rpc semver major version * chore: housekeeping
1303 lines
36 KiB
JavaScript
1303 lines
36 KiB
JavaScript
/* @license This file Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
|
|
It may be used under GPLv2 (SPDX: GPL-2.0-only).
|
|
License text can be found in the licenses/ folder. */
|
|
|
|
import { AboutDialog } from './about-dialog.js';
|
|
import { ContextMenu } from './context-menu.js';
|
|
import { Formatter } from './formatter.js';
|
|
import { Inspector } from './inspector.js';
|
|
import { MoveDialog } from './move-dialog.js';
|
|
import { OpenDialog } from './open-dialog.js';
|
|
import { OverflowMenu } from './overflow-menu.js';
|
|
import { Prefs } from './prefs.js';
|
|
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';
|
|
import {
|
|
TorrentRow,
|
|
TorrentRendererCompact,
|
|
TorrentRendererFull,
|
|
} from './torrent-row.js';
|
|
import {
|
|
newOpts,
|
|
icon,
|
|
debounce,
|
|
deepEqual,
|
|
setEnabled,
|
|
setTextContent,
|
|
} from './utils.js';
|
|
|
|
export class Transmission extends EventTarget {
|
|
constructor(action_manager, notifications, prefs) {
|
|
super();
|
|
|
|
// Initialize the helper classes
|
|
this.action_manager = action_manager;
|
|
this.handler = null;
|
|
this.notifications = notifications;
|
|
this.prefs = prefs;
|
|
this.remote = new Remote(this);
|
|
this.speed = {
|
|
down: document.querySelector('#speed-down'),
|
|
up: document.querySelector('#speed-up'),
|
|
};
|
|
|
|
for (const [selector, name] of [
|
|
['#toolbar-open', 'open'],
|
|
['#toolbar-delete', 'delete'],
|
|
['#toolbar-start', 'start'],
|
|
['#toolbar-pause', 'pause'],
|
|
['#toolbar-inspector', 'inspector'],
|
|
['#toolbar-overflow', 'overflow'],
|
|
]) {
|
|
document
|
|
.querySelector(selector)
|
|
.prepend(icon[name](), document.createElement('BR'));
|
|
}
|
|
|
|
document.querySelector('.speed-container').append(icon.speedDown());
|
|
document
|
|
.querySelector('.speed-container + .speed-container')
|
|
.append(icon.speedUp());
|
|
|
|
this.addEventListener('torrent-selection-changed', (event_) =>
|
|
this.action_manager.update(event_),
|
|
);
|
|
|
|
// Initialize the implementation fields
|
|
this.filterText = '';
|
|
this._torrents = {};
|
|
this._rows = [];
|
|
this.oldTrackers = [];
|
|
this.dirtyTorrents = new Set();
|
|
|
|
this.changeStatus = false;
|
|
this.refilterSoon = debounce(() => this._refilter(false));
|
|
this.refilterAllSoon = debounce(() => this._refilter(true));
|
|
|
|
this.pointer_device = Object.seal({
|
|
is_touch_device: 'ontouchstart' in globalThis,
|
|
long_press_callback: null,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
this.popup = Array.from({ length: Transmission.max_popups }).fill(null);
|
|
|
|
this.busytyping = false;
|
|
|
|
// listen to actions
|
|
// TODO: consider adding a mutator listener here to see dynamic additions
|
|
for (const element of document.querySelectorAll(`button[data-action]`)) {
|
|
const { action } = element.dataset;
|
|
setEnabled(element, this.action_manager.isEnabled(action));
|
|
element.addEventListener('click', () => {
|
|
this.action_manager.click(action);
|
|
});
|
|
}
|
|
|
|
document
|
|
.querySelector('#filter-tracker')
|
|
.addEventListener('change', (event_) => {
|
|
this.setFilterTracker(event_.target.value);
|
|
});
|
|
|
|
this.action_manager.addEventListener('change', (event_) => {
|
|
for (const element of document.querySelectorAll(
|
|
`[data-action="${event_.action}"]`,
|
|
)) {
|
|
setEnabled(element, event_.enabled);
|
|
}
|
|
});
|
|
|
|
this.action_manager.addEventListener('click', (event_) => {
|
|
switch (event_.action) {
|
|
case 'copy-name':
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(this.handler.subtree.name);
|
|
} else {
|
|
// navigator.clipboard requires HTTPS or localhost
|
|
// Emergency approach
|
|
prompt('Select all then copy', this.handler.subtree.name);
|
|
}
|
|
this.handler.classList.remove('selected');
|
|
break;
|
|
case 'deselect-all':
|
|
this._deselectAll();
|
|
break;
|
|
case 'move-bottom':
|
|
this._moveBottom();
|
|
break;
|
|
case 'move-down':
|
|
this._moveDown();
|
|
break;
|
|
case 'move-top':
|
|
this._moveTop();
|
|
break;
|
|
case 'move-up':
|
|
this._moveUp();
|
|
break;
|
|
case 'open-torrent':
|
|
this.setCurrentPopup(new OpenDialog(this, this.remote));
|
|
break;
|
|
case 'pause-all-torrents':
|
|
this._stopTorrents(this._getAllTorrents());
|
|
break;
|
|
case 'pause-selected-torrents':
|
|
this._stopTorrents(this.getSelectedTorrents());
|
|
break;
|
|
case 'reannounce-selected-torrents':
|
|
this._reannounceTorrents(this.getSelectedTorrents());
|
|
break;
|
|
case 'remove-selected-torrents':
|
|
this._removeSelectedTorrents();
|
|
break;
|
|
case 'resume-selected-torrents':
|
|
this._startSelectedTorrents(false);
|
|
break;
|
|
case 'resume-selected-torrents-now':
|
|
this._startSelectedTorrents(true);
|
|
break;
|
|
case 'select-all':
|
|
this._selectAll();
|
|
break;
|
|
case 'show-about-dialog':
|
|
this.setCurrentPopup(new AboutDialog(this.version_info));
|
|
break;
|
|
case 'show-inspector':
|
|
if (this.popup[0] instanceof Inspector) {
|
|
this.popup[0].close();
|
|
} else {
|
|
this.setCurrentPopup(new Inspector(this), 0);
|
|
}
|
|
break;
|
|
case 'show-move-dialog':
|
|
this.setCurrentPopup(new MoveDialog(this, this.remote));
|
|
break;
|
|
case 'show-overflow-menu':
|
|
if (
|
|
this.popup[Transmission.default_popup_level] instanceof OverflowMenu
|
|
) {
|
|
this.popup[Transmission.default_popup_level].close();
|
|
} else {
|
|
this.setCurrentPopup(
|
|
new OverflowMenu(
|
|
this,
|
|
this.prefs,
|
|
this.remote,
|
|
this.action_manager,
|
|
),
|
|
);
|
|
}
|
|
break;
|
|
case 'show-preferences-dialog':
|
|
this.setCurrentPopup(new PrefsDialog(this, this.remote), 0);
|
|
break;
|
|
case 'show-shortcuts-dialog':
|
|
this.setCurrentPopup(new ShortcutsDialog(this.action_manager));
|
|
break;
|
|
case 'show-statistics-dialog':
|
|
this.setCurrentPopup(new StatisticsDialog(this.remote));
|
|
break;
|
|
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;
|
|
case 'toggle-compact-rows':
|
|
this.prefs.display_mode =
|
|
this.prefs.display_mode === Prefs.DisplayCompact
|
|
? Prefs.DisplayFull
|
|
: Prefs.DisplayCompact;
|
|
break;
|
|
case 'verify-selected-torrents':
|
|
this._verifyTorrents(this.getSelectedTorrents());
|
|
break;
|
|
default:
|
|
console.warn(`unhandled action: ${event_.action}`);
|
|
}
|
|
});
|
|
|
|
let e = document.querySelector('#filter-mode');
|
|
// Initialize filter options
|
|
newOpts(e, null, [['All', Prefs.FilterAll]]);
|
|
newOpts(e, 'status', [
|
|
['Active', Prefs.FilterActive],
|
|
['Downloading', Prefs.FilterDownloading],
|
|
['Seeding', Prefs.FilterSeeding],
|
|
['Paused', Prefs.FilterPaused],
|
|
['Finished', Prefs.FilterFinished],
|
|
['Error', Prefs.FilterError],
|
|
]);
|
|
newOpts(e, 'list', [
|
|
['Private torrents', Prefs.FilterPrivate],
|
|
['Public torrents', Prefs.FilterPublic],
|
|
]);
|
|
|
|
// listen to filter changes
|
|
e.value = this.prefs.filter_mode;
|
|
e.addEventListener('change', (event_) => {
|
|
this.prefs.filter_mode = event_.target.value;
|
|
this.refilterAllSoon();
|
|
});
|
|
|
|
e = document.querySelector('#filter-tracker');
|
|
newOpts(e, null, [['All', Prefs.FilterAll]]);
|
|
|
|
const s = document.querySelector('#torrent-search');
|
|
e = document.querySelector('#reset');
|
|
e.addEventListener('click', () => {
|
|
s.value = '';
|
|
this._setFilterText(s.value);
|
|
this.refilterAllSoon();
|
|
});
|
|
|
|
if (s.value.trim()) {
|
|
this.filterText = s.value;
|
|
e.style.display = 'block';
|
|
this.refilterAllSoon();
|
|
}
|
|
|
|
e = document.querySelector('#turtle');
|
|
e.addEventListener('click', (event_) => {
|
|
this.remote.savePrefs({
|
|
[RPC._TurtleState]:
|
|
!event_.target.classList.contains('alt-speed-enabled'),
|
|
});
|
|
});
|
|
|
|
document.addEventListener('keydown', this._keyDown.bind(this));
|
|
document.addEventListener('keyup', this._keyUp.bind(this));
|
|
e = document.querySelector('#torrent-container');
|
|
e.addEventListener('click', (e_) => {
|
|
if (this.popup[Transmission.default_popup_level]) {
|
|
this.setCurrentPopup(null);
|
|
}
|
|
if (e_.target === e_.currentTarget) {
|
|
this._deselectAll();
|
|
}
|
|
});
|
|
e.addEventListener('dblclick', () => {
|
|
if (!this.popup[0] || this.popup[0].name !== 'inspector') {
|
|
this.action_manager.click('show-inspector');
|
|
}
|
|
});
|
|
e.addEventListener('dragenter', Transmission._dragenter);
|
|
e.addEventListener('dragover', Transmission._dragenter);
|
|
e.addEventListener('drop', this._drop.bind(this));
|
|
this._setupSearchBox();
|
|
|
|
this.elements = {
|
|
torrent_list: document.querySelector('#torrent-list'),
|
|
};
|
|
|
|
const right_click = (event_) => {
|
|
// if not already, highlight the torrent
|
|
let row_element = event_.target;
|
|
while (row_element && !row_element.classList.contains('torrent')) {
|
|
row_element = row_element.parentNode;
|
|
}
|
|
const row = this._rows.find((r) => r.getElement() === row_element);
|
|
if (row && !row.isSelected()) {
|
|
this._setSelectedRow(row);
|
|
}
|
|
|
|
if (this.handler) {
|
|
this.handler.classList.remove('selected');
|
|
this.handler = null;
|
|
}
|
|
|
|
this.context_menu('#torrent-container');
|
|
event_.preventDefault();
|
|
};
|
|
|
|
this.pointer_event(this.elements.torrent_list, right_click);
|
|
|
|
// Get preferences & torrents from the daemon
|
|
this.loadDaemonPrefs();
|
|
this._initializeTorrents();
|
|
this.refreshTorrents();
|
|
this.togglePeriodicSessionRefresh(true);
|
|
|
|
// this.updateButtonsSoon();
|
|
|
|
this.prefs.addEventListener('change', ({ key, value }) =>
|
|
this._onPrefChanged(key, value),
|
|
);
|
|
for (const [key, value] of this.prefs.entries()) {
|
|
this._onPrefChanged(key, value);
|
|
}
|
|
}
|
|
|
|
_openTorrentFromUrl() {
|
|
setTimeout(() => {
|
|
const addTorrent = new URLSearchParams(globalThis.location.search).get(
|
|
'addtorrent',
|
|
);
|
|
if (addTorrent) {
|
|
this.setCurrentPopup(new OpenDialog(this, this.remote, addTorrent));
|
|
const newUrl = new URL(globalThis.location);
|
|
newUrl.search = '';
|
|
globalThis.history.pushState('', '', newUrl.toString());
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
loadDaemonPrefs() {
|
|
this.remote.loadDaemonPrefs((data) => {
|
|
this.session_properties = data.result;
|
|
this._openTorrentFromUrl();
|
|
});
|
|
}
|
|
|
|
get session_properties() {
|
|
return this._session_properties;
|
|
}
|
|
set session_properties(o) {
|
|
if (deepEqual(this._session_properties, o)) {
|
|
return;
|
|
}
|
|
|
|
this._session_properties = Object.seal(o);
|
|
const event = new Event('session-change');
|
|
event.session_properties = o;
|
|
this.dispatchEvent(event);
|
|
|
|
// TODO: maybe have this in a listener handler?
|
|
this._updateGuiFromSession(o);
|
|
}
|
|
|
|
_setupSearchBox() {
|
|
const e = document.querySelector('#torrent-search');
|
|
const blur_token = 'blur';
|
|
e.classList.add(blur_token);
|
|
e.addEventListener('blur', () => e.classList.add(blur_token));
|
|
e.addEventListener('focus', () => e.classList.remove(blur_token));
|
|
e.addEventListener('input', () => {
|
|
if (e.value.trim() !== this.filterText) {
|
|
this._setFilterText(e.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
_onPrefChanged(key, value) {
|
|
switch (key) {
|
|
case Prefs.DisplayMode: {
|
|
this.torrentRenderer =
|
|
value === 'compact'
|
|
? new TorrentRendererCompact()
|
|
: new TorrentRendererFull();
|
|
this.refilterAllSoon();
|
|
break;
|
|
}
|
|
case Prefs.ContrastMode: {
|
|
// Add custom class to the body/html element to get the appropriate contrast color scheme
|
|
document.body.classList.remove('contrast-more', 'contrast-less');
|
|
document.body.classList.add(`contrast-${value}`);
|
|
// this.refilterAllSoon();
|
|
break;
|
|
}
|
|
|
|
case Prefs.FilterMode:
|
|
case Prefs.SortDirection:
|
|
case Prefs.SortMode:
|
|
this.refilterAllSoon();
|
|
break;
|
|
|
|
case Prefs.RefreshRate: {
|
|
clearInterval(this.refreshTorrentsInterval);
|
|
const callback = this.refreshTorrents.bind(this);
|
|
const pref = this.prefs.refresh_rate_sec;
|
|
const msec = pref > 0 ? pref * 1000 : 1000;
|
|
this.refreshTorrentsInterval = setInterval(callback, msec);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
/*noop*/
|
|
break;
|
|
}
|
|
}
|
|
|
|
context_menu(container_id, menu_items) {
|
|
// open context menu
|
|
const popup = new ContextMenu(this, menu_items);
|
|
this.setCurrentPopup(popup);
|
|
|
|
const bounds = document.querySelector(container_id).getBoundingClientRect();
|
|
const x = Math.min(
|
|
this.pointer_device.x,
|
|
bounds.right + globalThis.scrollX - popup.root.clientWidth,
|
|
);
|
|
const y = Math.min(
|
|
this.pointer_device.y,
|
|
bounds.bottom + globalThis.scrollY - popup.root.clientHeight,
|
|
);
|
|
popup.root.style.left = `${Math.max(x, 0)}px`;
|
|
popup.root.style.top = `${Math.max(y, 0)}px`;
|
|
}
|
|
|
|
pointer_event(e_, right_click) {
|
|
if (this.pointer_device.is_touch_device) {
|
|
const touch = this.pointer_device;
|
|
e_.addEventListener('touchstart', (event_) => {
|
|
touch.x = event_.touches[0].pageX;
|
|
touch.y = event_.touches[0].pageY;
|
|
|
|
if (touch.long_press_callback) {
|
|
clearTimeout(touch.long_press_callback);
|
|
touch.long_press_callback = null;
|
|
} else {
|
|
touch.long_press_callback = setTimeout(() => {
|
|
if (event_.touches.length === 1) {
|
|
right_click(event_);
|
|
}
|
|
}, 500);
|
|
}
|
|
});
|
|
e_.addEventListener('touchend', () => {
|
|
clearTimeout(touch.long_press_callback);
|
|
touch.long_press_callback = null;
|
|
setTimeout(() => {
|
|
const popup = this.popup[Transmission.default_popup_level];
|
|
if (popup) {
|
|
popup.root.style.pointerEvents = 'auto';
|
|
}
|
|
}, 1);
|
|
});
|
|
e_.addEventListener('touchmove', (event_) => {
|
|
touch.x = event_.touches[0].pageX;
|
|
touch.y = event_.touches[0].pageY;
|
|
|
|
clearTimeout(touch.long_press_callback);
|
|
touch.long_press_callback = null;
|
|
});
|
|
e_.addEventListener('contextmenu', (event_) => {
|
|
event_.preventDefault();
|
|
});
|
|
} else {
|
|
e_.addEventListener('mousemove', (event_) => {
|
|
this.pointer_device.x = event_.pageX;
|
|
this.pointer_device.y = event_.pageY;
|
|
});
|
|
e_.addEventListener('contextmenu', (event_) => {
|
|
right_click(event_);
|
|
const popup = this.popup[Transmission.default_popup_level];
|
|
if (popup) {
|
|
popup.root.style.pointerEvents = 'auto';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// UTILITIES
|
|
|
|
static get max_popups() {
|
|
return 2;
|
|
}
|
|
|
|
static get default_popup_level() {
|
|
return Transmission.max_popups - 1;
|
|
}
|
|
|
|
_getAllTorrents() {
|
|
return Object.values(this._torrents);
|
|
}
|
|
|
|
static _getTorrentIds(torrents) {
|
|
return torrents.map((t) => t.getId());
|
|
}
|
|
|
|
seedRatioLimit() {
|
|
const p = this.session_properties;
|
|
if (p && p.seed_ratio_limited) {
|
|
return p.seed_ratio_limit;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// SELECTION
|
|
|
|
_getSelectedRows() {
|
|
return this._rows.filter((r) => r.isSelected());
|
|
}
|
|
|
|
getSelectedTorrents() {
|
|
return this._getSelectedRows().map((r) => r.getTorrent());
|
|
}
|
|
|
|
_getSelectedTorrentIds() {
|
|
return Transmission._getTorrentIds(this.getSelectedTorrents());
|
|
}
|
|
|
|
_setSelectedRow(row) {
|
|
const e_sel = row ? row.getElement() : null;
|
|
for (const e of this.elements.torrent_list.children) {
|
|
e.classList.toggle('selected', e === e_sel);
|
|
}
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
|
|
_selectRow(row) {
|
|
row.getElement().classList.add('selected');
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
|
|
_deselectRow(row) {
|
|
row.getElement().classList.remove('selected');
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
|
|
_selectAll() {
|
|
for (const e of this.elements.torrent_list.children) {
|
|
e.classList.add('selected');
|
|
}
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
|
|
_deselectAll() {
|
|
for (const e of this.elements.torrent_list.children) {
|
|
e.classList.remove('selected');
|
|
}
|
|
this._dispatchSelectionChanged();
|
|
delete this._last_torrent_clicked;
|
|
}
|
|
|
|
_indexOfLastTorrent() {
|
|
return this._rows.findIndex(
|
|
(row) => row.getTorrentId() === this._last_torrent_clicked,
|
|
);
|
|
}
|
|
|
|
// Select a range from this row to the last clicked torrent
|
|
_selectRange(row) {
|
|
const last = this._indexOfLastTorrent();
|
|
|
|
if (last === -1) {
|
|
this._selectRow(row);
|
|
} else {
|
|
// select the range between the previous & current
|
|
const next = this._rows.indexOf(row);
|
|
const min = Math.min(last, next);
|
|
const max = Math.max(last, next);
|
|
for (let index = min; index <= max; ++index) {
|
|
this._selectRow(this._rows[index]);
|
|
}
|
|
}
|
|
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
|
|
_dispatchSelectionChanged() {
|
|
const nonselected = [];
|
|
const selected = [];
|
|
for (const r of this._rows) {
|
|
(r.isSelected() ? selected : nonselected).push(r.getTorrent());
|
|
}
|
|
|
|
const event = new Event('torrent-selection-changed');
|
|
event.nonselected = nonselected;
|
|
event.selected = selected;
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/*--------------------------------------------
|
|
*
|
|
* E V E N T F U N C T I O N S
|
|
*
|
|
*--------------------------------------------*/
|
|
|
|
static _createKeyShortcutFromKeyboardEvent(event_) {
|
|
const a = [];
|
|
if (event_.ctrlKey) {
|
|
a.push('Control');
|
|
}
|
|
if (event_.altKey) {
|
|
a.push('Alt');
|
|
}
|
|
if (event_.metaKey) {
|
|
a.push('Meta');
|
|
}
|
|
if (event_.shiftKey) {
|
|
a.push('Shift');
|
|
}
|
|
a.push(event_.key.length === 1 ? event_.key.toUpperCase() : event_.key);
|
|
return a.join('+');
|
|
}
|
|
|
|
// Process key events
|
|
_keyDown(event_) {
|
|
const { ctrlKey, keyCode, metaKey, shiftKey, target } = event_;
|
|
|
|
// look for a shortcut
|
|
const is_input_focused = ['INPUT', 'TEXTAREA'].includes(target.tagName);
|
|
if (!is_input_focused) {
|
|
const shortcut = Transmission._createKeyShortcutFromKeyboardEvent(event_);
|
|
const action = this.action_manager.getActionForShortcut(shortcut);
|
|
if (action) {
|
|
event_.preventDefault();
|
|
this.action_manager.click(action);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const esc_key = keyCode === 27; // esc key pressed
|
|
if (esc_key && this.popup.some(Boolean)) {
|
|
this.setCurrentPopup(null, 0);
|
|
event_.preventDefault();
|
|
return;
|
|
}
|
|
|
|
const any_popup_active = document.querySelector('.popup:not(.hidden)');
|
|
const rows = this._rows;
|
|
|
|
// Some shortcuts can only be used if the following conditions are met:
|
|
// 1. when no input fields are focused
|
|
// 2. when no other dialogs are visible
|
|
// 3. when the meta or ctrl key isn't pressed (i.e. opening dev tools shouldn't trigger the info panel)
|
|
if (!is_input_focused && !any_popup_active && !metaKey && !ctrlKey) {
|
|
const shift_key = keyCode === 16; // shift key pressed
|
|
const up_key = keyCode === 38; // up key pressed
|
|
const dn_key = keyCode === 40; // down key pressed
|
|
if ((up_key || dn_key) && rows.length > 0) {
|
|
const last = this._indexOfLastTorrent();
|
|
const anchor = this._shift_index;
|
|
const min = 0;
|
|
const max = rows.length - 1;
|
|
let index = last;
|
|
|
|
if (dn_key && index + 1 <= max) {
|
|
++index;
|
|
} else if (up_key && index - 1 >= min) {
|
|
--index;
|
|
}
|
|
|
|
const r = rows[index];
|
|
|
|
if (anchor >= 0) {
|
|
// user is extending the selection
|
|
// with the shift + arrow keys...
|
|
if (
|
|
(anchor <= last && last < index) ||
|
|
(anchor >= last && last > index)
|
|
) {
|
|
this._selectRow(r);
|
|
} else if (
|
|
(anchor >= last && index > last) ||
|
|
(anchor <= last && last > index)
|
|
) {
|
|
this._deselectRow(rows[last]);
|
|
}
|
|
} else {
|
|
if (shiftKey) {
|
|
this._selectRange(r);
|
|
} else {
|
|
this._setSelectedRow(r);
|
|
}
|
|
}
|
|
if (r) {
|
|
this._last_torrent_clicked = r.getTorrentId();
|
|
r.getElement().scrollIntoView();
|
|
event_.preventDefault();
|
|
}
|
|
} else if (shift_key) {
|
|
this._shift_index = this._indexOfLastTorrent();
|
|
}
|
|
}
|
|
}
|
|
|
|
_keyUp(event_) {
|
|
if (event_.keyCode === 16) {
|
|
// shift key pressed
|
|
delete this._shift_index;
|
|
}
|
|
}
|
|
|
|
static _dragenter(event_) {
|
|
if (event_.dataTransfer && event_.dataTransfer.types) {
|
|
const copy_types = new Set(['text/uri-list', 'text/plain']);
|
|
if (
|
|
event_.dataTransfer.types.some((type) => copy_types.has(type)) ||
|
|
event_.dataTransfer.types.includes('Files')
|
|
) {
|
|
event_.stopPropagation();
|
|
event_.preventDefault();
|
|
event_.dataTransfer.dropEffect = 'copy';
|
|
return false;
|
|
}
|
|
} else if (event_.dataTransfer) {
|
|
event_.dataTransfer.dropEffect = 'none';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static _isValidURL(string) {
|
|
try {
|
|
const url = new URL(string);
|
|
return Boolean(url);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
shouldAddedTorrentsStart() {
|
|
return this.session_properties.start_added_torrents;
|
|
}
|
|
|
|
_drop(event_) {
|
|
const paused = !this.shouldAddedTorrentsStart();
|
|
|
|
if (!event_.dataTransfer || !event_.dataTransfer.types) {
|
|
return true;
|
|
}
|
|
|
|
const type = event_.dataTransfer.types.findLast((t) =>
|
|
['text/uri-list', 'text/plain'].includes(t),
|
|
);
|
|
for (const uri of event_.dataTransfer
|
|
.getData(type)
|
|
.split('\n')
|
|
.map((string) => string.trim())
|
|
.filter((string) => Transmission._isValidURL(string))) {
|
|
this.remote.addTorrentByUrl(uri, paused);
|
|
}
|
|
|
|
const { files } = event_.dataTransfer;
|
|
|
|
if (files.length > 0) {
|
|
this.setCurrentPopup(new OpenDialog(this, this.remote, '', files));
|
|
}
|
|
event_.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
// turn the periodic ajax session refresh on & off
|
|
togglePeriodicSessionRefresh(enabled) {
|
|
if (!enabled && this.sessionInterval) {
|
|
clearInterval(this.sessionInterval);
|
|
delete this.sessionInterval;
|
|
}
|
|
if (enabled) {
|
|
this.loadDaemonPrefs();
|
|
if (!this.sessionInterval) {
|
|
const msec = 8000;
|
|
this.sessionInterval = setInterval(
|
|
this.loadDaemonPrefs.bind(this),
|
|
msec,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
_setFilterText(search) {
|
|
clearTimeout(this.busytyping);
|
|
this.busytyping = setTimeout(
|
|
() => {
|
|
this.busytyping = false;
|
|
this.filterText = search.trim();
|
|
this.refilterAllSoon();
|
|
},
|
|
search ? 250 : 0,
|
|
);
|
|
}
|
|
|
|
_onTorrentChanged(event_) {
|
|
if (this.changeStatus) {
|
|
this._dispatchSelectionChanged();
|
|
this.changeStatus = false;
|
|
}
|
|
|
|
// update our dirty fields
|
|
const tor = event_.currentTarget;
|
|
this.dirtyTorrents.add(tor.getId());
|
|
|
|
// enqueue ui refreshes
|
|
this.refilterSoon();
|
|
}
|
|
|
|
updateTorrents(ids, fields) {
|
|
this.remote.updateTorrents(ids, fields, (table, removed_ids) => {
|
|
const needinfo = [];
|
|
|
|
const keys = table.shift();
|
|
const o = {};
|
|
for (const row of table) {
|
|
for (const [index, key] of keys.entries()) {
|
|
o[key] = row[index];
|
|
}
|
|
const { id } = o;
|
|
let t = this._torrents[id];
|
|
if (t) {
|
|
const needed = t.needsMetaData();
|
|
t.refresh(o);
|
|
if (needed && !t.needsMetaData()) {
|
|
needinfo.push(id);
|
|
}
|
|
} else {
|
|
t = this._torrents[id] = new Torrent(o);
|
|
t.addEventListener('dataChanged', this._onTorrentChanged.bind(this));
|
|
this.dirtyTorrents.add(id);
|
|
// do we need more info for this torrent?
|
|
if (!('name' in t.fields) || !('status' in t.fields)) {
|
|
needinfo.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needinfo.length > 0) {
|
|
// whee, new torrents! get their initial information.
|
|
const more_fields = [
|
|
'id',
|
|
...Torrent.Fields.Metadata,
|
|
...Torrent.Fields.Stats,
|
|
];
|
|
this.updateTorrents(needinfo, more_fields);
|
|
this.refilterSoon();
|
|
}
|
|
|
|
if (removed_ids) {
|
|
this._deleteTorrents(removed_ids);
|
|
this.refilterSoon();
|
|
}
|
|
});
|
|
}
|
|
/*
|
|
TODO: fix this when notifications get fixed
|
|
t.notifyOnFieldChange('status', (newValue, oldValue) => {
|
|
if (
|
|
oldValue === Torrent._StatusDownload &&
|
|
(newValue === Torrent._StatusSeed || newValue === Torrent._StatusSeedWait)
|
|
) {
|
|
$(this).trigger('downloadComplete', [t]);
|
|
} else if (
|
|
oldValue === Torrent._StatusSeed &&
|
|
newValue === Torrent._StatusStopped &&
|
|
t.isFinished()
|
|
) {
|
|
$(this).trigger('seedingComplete', [t]);
|
|
} else {
|
|
$(this).trigger('statusChange', [t]);
|
|
}
|
|
});
|
|
*/
|
|
|
|
refreshTorrents() {
|
|
const fields = ['id', ...Torrent.Fields.Stats];
|
|
this.updateTorrents('recently_active', fields);
|
|
}
|
|
|
|
_initializeTorrents() {
|
|
const fields = ['id', ...Torrent.Fields.Metadata, ...Torrent.Fields.Stats];
|
|
this.updateTorrents(null, fields);
|
|
}
|
|
|
|
_onRowClicked(event_) {
|
|
const meta_key = event_.metaKey || event_.ctrlKey,
|
|
{ row } = event_.currentTarget;
|
|
|
|
if (this.popup[Transmission.default_popup_level]) {
|
|
this.setCurrentPopup(null);
|
|
}
|
|
|
|
// Prevents click carrying to parent element
|
|
// which deselects all on click
|
|
event_.stopPropagation();
|
|
|
|
if (event_.shiftKey) {
|
|
this._selectRange(row);
|
|
// Need to deselect any selected text
|
|
globalThis.focus();
|
|
|
|
// Apple-Click, not selected
|
|
} else if (!row.isSelected() && meta_key) {
|
|
this._selectRow(row);
|
|
|
|
// Regular Click, not selected
|
|
} else if (!row.isSelected()) {
|
|
this._setSelectedRow(row);
|
|
|
|
// Apple-Click, selected
|
|
} else if (row.isSelected() && meta_key) {
|
|
this._deselectRow(row);
|
|
|
|
// Regular Click, selected
|
|
} else if (row.isSelected()) {
|
|
this._setSelectedRow(row);
|
|
}
|
|
|
|
this._last_torrent_clicked = row.getTorrentId();
|
|
}
|
|
|
|
_deleteTorrents(ids) {
|
|
if (ids && ids.length > 0) {
|
|
for (const id of ids) {
|
|
this.dirtyTorrents.add(id);
|
|
delete this._torrents[id];
|
|
}
|
|
this.refilterSoon();
|
|
}
|
|
}
|
|
|
|
_removeSelectedTorrents() {
|
|
const torrents = this.getSelectedTorrents();
|
|
if (torrents.length > 0) {
|
|
this.setCurrentPopup(new RemoveDialog({ remote: this.remote, torrents }));
|
|
}
|
|
}
|
|
|
|
_startSelectedTorrents(force) {
|
|
this._startTorrents(this.getSelectedTorrents(), force);
|
|
}
|
|
|
|
_startTorrents(torrents, force) {
|
|
this.changeStatus = true;
|
|
this.remote.startTorrents(
|
|
Transmission._getTorrentIds(torrents),
|
|
force,
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
_verifyTorrents(torrents) {
|
|
this.remote.verifyTorrents(
|
|
Transmission._getTorrentIds(torrents),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
|
|
_reannounceTorrents(torrents) {
|
|
this.remote.reannounceTorrents(
|
|
Transmission._getTorrentIds(torrents),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
|
|
_stopTorrents(torrents) {
|
|
this.changeStatus = true;
|
|
this.remote.stopTorrents(
|
|
Transmission._getTorrentIds(torrents),
|
|
() => {
|
|
setTimeout(() => {
|
|
this.refreshTorrents();
|
|
}, 500);
|
|
},
|
|
this,
|
|
);
|
|
}
|
|
changeFileCommand(torrentId, rowIndices, command) {
|
|
this.remote.changeFileCommand(torrentId, rowIndices, command);
|
|
}
|
|
|
|
// Queue
|
|
_moveTop() {
|
|
this.remote.moveTorrentsToTop(
|
|
this._getSelectedTorrentIds(),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
_moveUp() {
|
|
this.remote.moveTorrentsUp(
|
|
this._getSelectedTorrentIds(),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
_moveDown() {
|
|
this.remote.moveTorrentsDown(
|
|
this._getSelectedTorrentIds(),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
_moveBottom() {
|
|
this.remote.moveTorrentsToBottom(
|
|
this._getSelectedTorrentIds(),
|
|
this.refreshTorrents,
|
|
this,
|
|
);
|
|
}
|
|
|
|
///
|
|
|
|
_updateGuiFromSession(o) {
|
|
const [, version, checksum] = o.version.match(/^(.*)\s\(([\da-f]+)\)/);
|
|
this.version_info = {
|
|
checksum,
|
|
version,
|
|
};
|
|
|
|
const element = document.querySelector('#turtle');
|
|
element.classList.toggle('alt-speed-enabled', o[RPC._TurtleState]);
|
|
}
|
|
|
|
_updateStatusbar() {
|
|
const fmt = Formatter;
|
|
const torrents = this._getAllTorrents();
|
|
|
|
const u = torrents.reduce(
|
|
(accumulator, tor) => accumulator + tor.getUploadSpeed(),
|
|
0,
|
|
);
|
|
const d = torrents.reduce(
|
|
(accumulator, tor) => accumulator + tor.getDownloadSpeed(),
|
|
0,
|
|
);
|
|
const string = fmt.countString('Transfer', 'Transfers', this._rows.length);
|
|
|
|
setTextContent(this.speed.down, fmt.speedBps(d));
|
|
setTextContent(this.speed.up, fmt.speedBps(u));
|
|
setTextContent(document.querySelector('#filter-count'), string);
|
|
}
|
|
|
|
static _displayName(hostname) {
|
|
let name = hostname;
|
|
if (name.length > 0) {
|
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
_updateFilterSelect() {
|
|
const trackers = this._getTrackerCounts();
|
|
const sitenames = Object.keys(trackers).toSorted();
|
|
|
|
// Update select box only when list of trackers has changed
|
|
if (
|
|
sitenames.length !== this.oldTrackers.length ||
|
|
sitenames.some((ele, idx) => ele !== this.oldTrackers[idx])
|
|
) {
|
|
this.oldTrackers = sitenames;
|
|
|
|
const a = [
|
|
['All', Prefs.FilterAll, !this.filterTracker],
|
|
...sitenames.map((sitename) => [
|
|
Transmission._displayName(sitename),
|
|
sitename,
|
|
sitename === this.filterTracker,
|
|
]),
|
|
];
|
|
|
|
const e = document.querySelector('#filter-tracker');
|
|
while (e.firstChild) {
|
|
e.lastChild.remove();
|
|
}
|
|
newOpts(e, null, a);
|
|
}
|
|
}
|
|
|
|
/// FILTER
|
|
|
|
sortRows(rows) {
|
|
const torrents = rows.map((row) => row.getTorrent());
|
|
const id2row = rows.reduce((accumulator, row) => {
|
|
accumulator[row.getTorrent().getId()] = row;
|
|
return accumulator;
|
|
}, {});
|
|
Torrent.sortTorrents(
|
|
torrents,
|
|
this.prefs.sort_mode,
|
|
this.prefs.sort_direction,
|
|
);
|
|
for (const [index, tor] of torrents.entries()) {
|
|
rows[index] = id2row[tor.getId()];
|
|
}
|
|
}
|
|
|
|
_refilter(rebuildEverything) {
|
|
const { sort_mode, sort_direction, filter_mode } = this.prefs;
|
|
const filter_tracker = this.filterTracker;
|
|
const renderer = this.torrentRenderer;
|
|
const list = this.elements.torrent_list;
|
|
|
|
let filter_text = null;
|
|
let labels = null;
|
|
// TODO: This regex is wrong and is about to be removed in https://github.com/transmission/transmission/pull/7008,
|
|
// so it is left alone for now.
|
|
// eslint-disable-next-line sonarjs/slow-regex
|
|
const m = /^labels:([\w,-\s]*)(.*)$/.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(
|
|
(n, e) => (n + e.classList.contains('selected') ? 1 : 0),
|
|
0,
|
|
);
|
|
const old_row_count = countRows();
|
|
const old_sel_count = countSelectedRows();
|
|
|
|
this._updateFilterSelect();
|
|
|
|
if (rebuildEverything) {
|
|
while (list.firstChild) {
|
|
list.firstChild.remove();
|
|
}
|
|
this._rows = [];
|
|
this.dirtyTorrents = new Set(Object.keys(this._torrents));
|
|
|
|
document.querySelector('#reset').style.display =
|
|
this.filterText.length > 0 ? 'block' : 'none';
|
|
}
|
|
|
|
// rows that overlap with dirtyTorrents need to be refiltered.
|
|
// those that don't are 'clean' and don't need refiltering.
|
|
const clean_rows = [];
|
|
let dirty_rows = [];
|
|
for (const row of this._rows) {
|
|
if (this.dirtyTorrents.has(row.getTorrentId())) {
|
|
dirty_rows.push(row);
|
|
} else {
|
|
clean_rows.push(row);
|
|
}
|
|
}
|
|
|
|
// remove the dirty rows from the dom
|
|
for (const row of dirty_rows) {
|
|
row.getElement().remove();
|
|
}
|
|
|
|
// drop any dirty rows that don't pass the filter test
|
|
const temporary = [];
|
|
for (const row of dirty_rows) {
|
|
const id = row.getTorrentId();
|
|
const t = this._torrents[id];
|
|
if (t && t.test(filter_mode, filter_tracker, filter_text, labels)) {
|
|
temporary.push(row);
|
|
}
|
|
this.dirtyTorrents.delete(id);
|
|
}
|
|
dirty_rows = temporary;
|
|
|
|
// make new rows for dirty torrents that pass the filter test
|
|
// 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_tracker, filter_text, labels)) {
|
|
const row = new TorrentRow(renderer, this, t);
|
|
const e = row.getElement();
|
|
e.row = row;
|
|
dirty_rows.push(row);
|
|
e.addEventListener('click', this._onRowClicked.bind(this));
|
|
}
|
|
}
|
|
|
|
// sort the dirty rows
|
|
this.sortRows(dirty_rows);
|
|
|
|
// now we have two sorted arrays of rows
|
|
// and can do a simple two-way sorted merge.
|
|
const rows = [];
|
|
const cmax = clean_rows.length;
|
|
const dmax = dirty_rows.length;
|
|
const frag = document.createDocumentFragment();
|
|
let ci = 0;
|
|
let di = 0;
|
|
while (ci !== cmax || di !== dmax) {
|
|
let push_clean = null;
|
|
if (ci === cmax) {
|
|
push_clean = false;
|
|
} else if (di === dmax) {
|
|
push_clean = true;
|
|
} else {
|
|
const c = Torrent.compareTorrents(
|
|
clean_rows[ci].getTorrent(),
|
|
dirty_rows[di].getTorrent(),
|
|
sort_mode,
|
|
sort_direction,
|
|
);
|
|
push_clean = c < 0;
|
|
}
|
|
|
|
if (push_clean) {
|
|
rows.push(clean_rows[ci++]);
|
|
} else {
|
|
const row = dirty_rows[di++];
|
|
const e = row.getElement();
|
|
|
|
if (ci === cmax) {
|
|
frag.append(e);
|
|
} else {
|
|
list.insertBefore(e, clean_rows[ci].getElement());
|
|
}
|
|
|
|
rows.push(row);
|
|
}
|
|
}
|
|
list.append(frag);
|
|
|
|
// update our implementation fields
|
|
this._rows = rows;
|
|
this.dirtyTorrents.clear();
|
|
|
|
this._updateStatusbar();
|
|
if (
|
|
old_sel_count !== countSelectedRows() ||
|
|
old_row_count !== countRows()
|
|
) {
|
|
this._dispatchSelectionChanged();
|
|
}
|
|
}
|
|
|
|
setFilterTracker(sitename) {
|
|
const e = document.querySelector('#filter-tracker');
|
|
e.value = sitename;
|
|
|
|
this.filterTracker = sitename === Prefs.FilterAll ? '' : sitename;
|
|
this.refilterAllSoon();
|
|
}
|
|
|
|
_getTrackerCounts() {
|
|
const counts = {};
|
|
|
|
for (const torrent of this._getAllTorrents()) {
|
|
for (const tracker of torrent.getTrackers()) {
|
|
const { sitename } = tracker;
|
|
counts[sitename] = (counts[sitename] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
return counts;
|
|
}
|
|
|
|
///
|
|
|
|
setCurrentPopup(popup, level = Transmission.default_popup_level) {
|
|
for (let index = level; index < Transmission.max_popups; index++) {
|
|
if (this.popup[index]) {
|
|
this.popup[index].close();
|
|
}
|
|
}
|
|
|
|
this.popup[level] = popup;
|
|
|
|
if (this.popup[level]) {
|
|
const listener = () => {
|
|
if (this.popup[level]) {
|
|
this.popup[level].removeEventListener('close', listener);
|
|
this.popup[level] = null;
|
|
}
|
|
};
|
|
this.popup[level].addEventListener('close', listener);
|
|
} else if (this.handler) {
|
|
this.handler.classList.remove('selected');
|
|
}
|
|
}
|
|
}
|