From 286bb0b7f18537b5e82e7135175e2e4bd0d87c49 Mon Sep 17 00:00:00 2001 From: Bilal Ahmed <45879053+bill-ahmed@users.noreply.github.com> Date: Mon, 11 Oct 2021 12:36:28 -0400 Subject: [PATCH] File priority for torrents (#140) * refactor p-tag styles, also use it in torrent dialog * add more fields in general tab * get and render file priority information * keep track of file index * parent can control priority of children - add support for endpoint - more mock data * user-friendly names for file priorities * prevent new render while selecting priority * fix some styles * table styling tweaks * checkbox beside each file and folder * show number of files chosen in torrent contents * improved styling for tree explorer - everything is aligned properly - prevent overflow at the top-level --- mock_backend/index.js | 25 ++++- .../file-system-tree-explorer.component.html | 64 +++++++++---- .../file-system-tree-explorer.component.scss | 29 ++++-- .../file-system-tree-explorer.component.ts | 28 +++++- .../add-torrent-dialog.component.html | 14 ++- .../add-torrent-dialog.component.scss | 8 +- .../file-system-dialog.component.scss | 6 +- .../torrent-info-dialog.component.css | 19 ++-- .../torrent-info-dialog.component.html | 64 +++++++++---- .../torrent-info-dialog.component.ts | 96 ++++++++++++------- .../app/application-config.service.ts | 11 +++ .../file-system/FileSystemNodes/Inode.ts | 6 ++ .../file-system/file-system.service.ts | 8 +- .../torrent-data-http.service.ts | 15 ++- .../torrent-data-store.service.ts | 7 ++ src/app/services/units-helper.service.ts | 9 +- .../torrents-table.component.html | 2 +- .../torrents-table.component.scss | 55 +---------- .../torrents-table.component.ts | 23 +---- src/assets/http_config.json | 1 + src/assets/http_config.prod.json | 1 + src/styles.scss | 49 +++++++++- src/utils/Helpers.ts | 24 +++++ src/utils/Interfaces.ts | 5 + 24 files changed, 391 insertions(+), 178 deletions(-) diff --git a/mock_backend/index.js b/mock_backend/index.js index 7adcd7ca..bd9eeebd 100644 --- a/mock_backend/index.js +++ b/mock_backend/index.js @@ -48,26 +48,43 @@ app.get('/api/v2/sync/maindata', function(req, res) { res.json(response); }); +const torrent_priorities = [0, 1, 6, 7]; + app.post('/api/v2/torrents/files', function(req, res) { let response = [{ name: "Ubuntu LTS 18.04/something.iso", /** File size (bytes) */ size: GetRandomInt(0, 900000000000), progress: Math.random(), - priority: 1, + priority: torrent_priorities[GetRandomInt(0, torrent_priorities.length)], is_seed: true, piece_range: [], availability: Math.random(), - }, { + index: 0, + }, + { name: "Ubuntu LTS 20.20/another.iso", /** File size (bytes) */ size: GetRandomInt(0, 900000000000), progress: Math.random(), - priority: 6, + priority: torrent_priorities[GetRandomInt(0, torrent_priorities.length)], + is_seed: false, + piece_range: [], + availability: Math.random(), + index: 1 + }, + { + name: "Ubuntu LTS 20.20/folder1/new.iso", + /** File size (bytes) */ + size: GetRandomInt(0, 900000000000), + progress: Math.random(), + priority: 7,//torrent_priorities[GetRandomInt(0, torrent_priorities.length)], is_seed: false, piece_range: [], availability: Math.random(), - }]; + index: 2 + } +]; res.json(response); }); diff --git a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.html b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.html index 8fa78c1c..9f62a2f3 100644 --- a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.html +++ b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.html @@ -4,13 +4,26 @@
  • + +
    -
    +
    {{node.name}}
    +
    + + Choose priority + + + {{ file_priorities_mapping[item] }} + + + +
    +
    Done: {{getNodeProgress(node)}}% @@ -27,27 +40,42 @@
  • -
    - -
    -
    - {{node.name}} -
    +
    + -
    -
    - Done: {{getNodeProgress(node)}}% +
    + +
    +
    + {{node.name}}
    -
    - {{getNodeSize(node)}} +
    + + Choose priority + + + {{ file_priorities_mapping[item] }} + + +
    -
    + +
    +
    + Done: {{getNodeProgress(node)}}% +
    + +
    + {{getNodeSize(node)}} +
    +
    +
      diff --git a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.scss b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.scss index a9ddc1ed..52e630a7 100644 --- a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.scss +++ b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.scss @@ -18,25 +18,36 @@ cursor: default; } - overflow-x: scroll; + word-wrap: break-word; } #expand_folder_button { pointer-events: none; } +.toggle_checkbox { + margin: 0 10px 0 20px; +} + .node-info { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: space-between; align-items: center; - width: 90%; - #node-title { - flex-grow: 1; - width: 70%; + width: 100%; + + padding: 0 10px; + + .node-title { word-wrap: break-word; + width: 360px; + } + + .priority_selection { + font-size: 10pt; + padding-top: 10px; } #progress, #fileSize { @@ -50,7 +61,13 @@ flex-direction: row; justify-content: space-evenly; + flex-grow: 0.1; + #progress, #fileSize { - margin: 0 8px; + margin: 0 2px; + width: 80px; + text-align: end; } + + #progress { width: 100px; } } diff --git a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.ts b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.ts index 67662aed..b4dd808d 100644 --- a/src/app/file-system-tree-explorer/file-system-tree-explorer.component.ts +++ b/src/app/file-system-tree-explorer/file-system-tree-explorer.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core'; +import { Component, OnInit, Input, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core'; import { NestedTreeControl } from '@angular/cdk/tree'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { FileSystemService, SerializedNode } from '../services/file-system/file-system.service'; @@ -14,6 +14,13 @@ import { ApplicationConfigService } from '../services/app/application-config.ser export class FileSystemTreeExplorerComponent implements OnChanges { @Input() directories: SerializedNode[]; @Input() showProgress: boolean = false; + @Input() allowSetPriority: boolean = false; + + /** When user changes the priority */ + @Output() onPriorityChange = new EventEmitter(); + + /** When user toggles the drop-down menu in order to choose a priority */ + @Output() onPriorityChangeToggled = new EventEmitter(); public isLoading = true; @@ -21,6 +28,9 @@ export class FileSystemTreeExplorerComponent implements OnChanges { public treeControl = new NestedTreeControl(node => node.children); public dataSource = new MatTreeNestedDataSource(); + public file_priorities = ApplicationConfigService.FILE_PRIORITY_OPTS; + public file_priorities_mapping = ApplicationConfigService.FILE_PRIORITY_OPTS_MAPPING; + private root: DirectoryNode; /** File System to keep track of the files in a torrent */ private expanded_nodes: Set = new Set(); @@ -35,6 +45,8 @@ export class FileSystemTreeExplorerComponent implements OnChanges { ngOnInit(): void { this._updateData(); + + console.log('allow set priority?', this.allowSetPriority) } ngOnChanges(changes: SimpleChanges) { @@ -43,10 +55,18 @@ export class FileSystemTreeExplorerComponent implements OnChanges { this._updateData(); } - if(changes.showProgress) { - this.showProgress = changes.showProgress.currentValue; - } + if(changes.showProgress) { this.showProgress = changes.showProgress.currentValue; } + if(changes.allowSetPriority) { this.allowSetPriority = changes.allowSetPriority.currentValue } + } + + handleCheckboxClick(node: SerializedNode) { + let o = node.progress; + node.progress = o === 0 ? 1 : 0 + + this.onPriorityChange.emit(node); } + handleFilePriorityChange(node: SerializedNode) { this.onPriorityChange.emit(node); } + handleFilePriorityToggled() { this.onPriorityChangeToggled.emit(''); } /** Refresh all filesystem data. This could potentially be an * expensive operation. diff --git a/src/app/modals/add-torrent-dialog/add-torrent-dialog.component.html b/src/app/modals/add-torrent-dialog/add-torrent-dialog.component.html index e9625e2a..9bfbeb43 100644 --- a/src/app/modals/add-torrent-dialog/add-torrent-dialog.component.html +++ b/src/app/modals/add-torrent-dialog/add-torrent-dialog.component.html @@ -47,21 +47,18 @@


      Save Location

      -

      Pick a location to save the files. If none is chosen, the default shown will be used instead.

      Download To... - folder_open -
      -
      @@ -72,10 +69,11 @@

      Save Location

    -

    Torrent Contents

    - - Try uploading some files first. - +
    +

    Torrent Contents

    +

      •  

    +

    {{getFilesToUploadString()}}

    +
    {{torrent.name}} +

    {{torrent.hash}}

    - - {{state()}} - Forced Start - + +
    + + + + + +
    -
    -
    -

    Name: {{torrent.name}}

    -

    Total Size: {{total_size()}}

    -

    Downloaded: {{downloaded()}} / {{total_size()}}

    -

    Uploaded: {{uploaded()}}

    -

    Ratio: {{ratio()}}

    +
    +
    +

    Name: {{torrent.name}}

    +

    Total Size: {{total_size()}}

    +

    Save Path: {{torrent.save_path}}

    +
    + +
    +

    Added On: {{added_on()}}

    +

    Completed On: {{completed_on()}}

    +

    Last Activity: {{last_activity()}}

    +
    + + -
    -

    Added On: {{added_on()}}

    -

    Completed On: {{completed_on()}}

    -

    Last Activity: {{last_activity()}}

    -

    Save Location: {{torrent.save_path}}

    +
    +
    +

    Downloaded: {{downloaded()}} / {{total_size()}}

    +

    Download Speed: {{dl_speed()}} ({{dl_speed_avg()}} avg.)

    +

    Download Limit: {{dl_limit()}}

    +

    Share Ratio: {{ratio()}}

    +
    +

    Uploaded: {{uploaded()}}

    +

    Upload Speed: {{up_speed()}} ({{up_speed_avg()}} avg.)

    +

    Upload Limit: {{up_limit()}}

    +
    +
    @@ -40,7 +65,12 @@

    + [allowSetPriority]="true" + [directories]="get_content_directories_as_advanced_nodes()" + + (onPriorityChange)="handleFilePriorityChange($event)" + (onPriorityChangeToggled)="handlePriorityChangeToggled()"> +

    diff --git a/src/app/modals/torrent-info-dialog/torrent-info-dialog.component.ts b/src/app/modals/torrent-info-dialog/torrent-info-dialog.component.ts index d6142be6..8a19e92c 100644 --- a/src/app/modals/torrent-info-dialog/torrent-info-dialog.component.ts +++ b/src/app/modals/torrent-info-dialog/torrent-info-dialog.component.ts @@ -9,6 +9,8 @@ import { TorrentDataStoreService } from '../../services/torrent-management/torre import { NetworkConnectionInformationService } from '../../services/network/network-connection-information.service'; import { FileSystemService, SerializedNode } from '../../services/file-system/file-system.service'; import DirectoryNode from 'src/app/services/file-system/FileSystemNodes/DirectoryNode'; +import { getClassForStatus } from 'src/utils/Helpers'; +import { SnackbarService } from 'src/app/services/notifications/snackbar.service'; @Component({ selector: 'app-torrent-info-dialog', @@ -26,9 +28,11 @@ export class TorrentInfoDialogComponent implements OnInit { private panelsOpen: Set = new Set(); private REFRESH_INTERVAL: any; + private allowDataRefresh = true; + constructor(@Inject(MAT_DIALOG_DATA) data: any, private units_helper: UnitsHelperService, private pp: PrettyPrintTorrentDataService, private theme: ThemeService, private data_store: TorrentDataStoreService, - private network_info: NetworkConnectionInformationService, private fs: FileSystemService) { + private network_info: NetworkConnectionInformationService, private fs: FileSystemService, private snackbar: SnackbarService) { this.torrent = data.torrent; } @@ -39,13 +43,7 @@ export class TorrentInfoDialogComponent implements OnInit { this.data_store.GetTorrentContents(this.torrent).toPromise().then(res => {this.updateTorrentContents(res)}); /** Refresh torrent contents data on the recommended interval */ - this.REFRESH_INTERVAL = setInterval(() => { - this.data_store.GetTorrentContents(this.torrent).subscribe(content => { - this.updateTorrentContents(content); - }); - }, - this.network_info.get_refresh_interval_from_network_type("medium") - ); + this.setRefreshInterval(); } ngOnDestroy(): void { @@ -53,15 +51,19 @@ export class TorrentInfoDialogComponent implements OnInit { } private async updateTorrentContents(content: TorrentContents[]): Promise { + if(!this.allowDataRefresh) return; + this.torrentContents = content; let intermediate_nodes = this.torrentContents.map(file => { return { + index: file.index, name: "", path: file.name, parentPath: '', size: file.size, progress: file.progress, + priority: file.priority, type: "File" } }) @@ -77,42 +79,70 @@ export class TorrentInfoDialogComponent implements OnInit { this.isLoading = false; } - get_content_directories_as_advanced_nodes(): SerializedNode[] { - return this.torrentContentsAsNodes; - } + handleFilePriorityChange(node: SerializedNode) { + let newPriority = node.priority; - added_on(): string { - return this.units_helper.GetDateString(this.torrent.added_on); - } + // Recursively collect list of indexes that need to be changed. + let indexes = this._filePriChangeHelper(node, []); - completed_on(): string { - return this.pp.pretty_print_completed_on(this.torrent.completion_on); - } + // Dedupe + indexes = [...new Set(indexes)]; - last_activity(): string { - return this.pp.pretty_print_completed_on(this.torrent.last_activity) + this.data_store.SetFilePriority(this.torrent, indexes, newPriority).subscribe(() => { + this.snackbar.enqueueSnackBar("Updated file priority."); + }); } - total_size(): string { - return this.units_helper.GetFileSizeString(this.torrent.total_size); - } + handlePriorityChangeToggled() { this.allowDataRefresh = !this.allowDataRefresh; } - downloaded(): string { - return this.units_helper.GetFileSizeString(this.torrent.downloaded); - } + /** Recursively update list of indexes with index + * of each node + */ + private _filePriChangeHelper(node: SerializedNode, indexes: any[]): any[] { + indexes.push(node.index); - uploaded(): string { - return this.units_helper.GetFileSizeString(this.torrent.uploaded) - } + if(node.children) { + for (let child of node.children) { + indexes = this._filePriChangeHelper(child, indexes); + } + } - ratio(): number { - return Math.round(((this.torrent.ratio) + Number.EPSILON) * 100) / 100; + return indexes; } - state(): string { - return this.pp.pretty_print_status(this.torrent.state); + private setRefreshInterval() { + this.REFRESH_INTERVAL = setInterval(() => { + this.data_store.GetTorrentContents(this.torrent).subscribe(content => { + this.updateTorrentContents(content); + }); + }, + this.network_info.get_refresh_interval_from_network_type("medium") + ); } + get_content_directories_as_advanced_nodes(): SerializedNode[] { return this.torrentContentsAsNodes; } + + added_on() { return this.units_helper.GetDateString(this.torrent.added_on); } + completed_on() { return this.pp.pretty_print_completed_on(this.torrent.completion_on); } + last_activity() { return this.pp.pretty_print_completed_on(this.torrent.last_activity); } + + total_size() { return this.units_helper.GetFileSizeString(this.torrent.total_size); } + + downloaded() { return this.units_helper.GetFileSizeString(this.torrent.downloaded); } + uploaded() { return this.units_helper.GetFileSizeString(this.torrent.uploaded); } + + dl_speed() { return this.units_helper.GetFileSizeString(this.torrent.dlspeed) + '/s'; } + up_speed() { return this.units_helper.GetFileSizeString(this.torrent.upspeed) + '/s'; } + dl_speed_avg() { return this.units_helper.GetFileSizeString(this.torrent.dl_speed_avg) + (this.torrent.dl_speed_avg ? '/s' : ''); } + up_speed_avg() { return this.units_helper.GetFileSizeString(this.torrent.up_speed_avg) + (this.torrent.up_speed_avg ? '/s' : ''); } + + dl_limit() { return this.units_helper.GetFileSizeString(this.torrent.dl_limit) + (this.torrent.dl_limit < 0 ? '' : '/s'); } + up_limit() { return this.units_helper.GetFileSizeString(this.torrent.up_limit) + (this.torrent.up_limit < 0 ? '' : '/s'); } + + ratio() { return Math.round(((this.torrent.ratio) + Number.EPSILON) * 100) / 100; } + + state() { return this.pp.pretty_print_status(this.torrent.state); } + openPanel(name: string): void { this.panelsOpen.add(name); } @@ -125,4 +155,6 @@ export class TorrentInfoDialogComponent implements OnInit { return this.panelsOpen.has(name); } + public getClassForStatus(t: Torrent): string { return getClassForStatus(t); } + } diff --git a/src/app/services/app/application-config.service.ts b/src/app/services/app/application-config.service.ts index 5623ccfb..187502df 100644 --- a/src/app/services/app/application-config.service.ts +++ b/src/app/services/app/application-config.service.ts @@ -44,6 +44,17 @@ export class ApplicationConfigService { /** All available columns for the torrent table */ static ALL_COLUMNS = ['select', 'Actions', ...ApplicationConfigService.TORRENT_TABLE_COLUMNS]; + /** Allowed file priority options + * @see https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-contents + */ + static FILE_PRIORITY_OPTS = [0, 1, 6, 7]; + static FILE_PRIORITY_OPTS_MAPPING = { + 0: 'Don\'t Download', + 1: 'Normal', + 6: 'High', + 7: 'Maximum' + } + private user_preferences: UserPreferences; private user_preference_obs = new BehaviorSubject({ }); diff --git a/src/app/services/file-system/FileSystemNodes/Inode.ts b/src/app/services/file-system/FileSystemNodes/Inode.ts index 9a2a6fb0..b499d7af 100644 --- a/src/app/services/file-system/FileSystemNodes/Inode.ts +++ b/src/app/services/file-system/FileSystemNodes/Inode.ts @@ -6,11 +6,13 @@ import { InvalidNameError } from '../Exceptions/FileSystemExceptions'; /** A class to represent object in a File System. */ export default class Inode extends TreeNode implements SerializableNode { + index: any; value: string; progress: number; children: Inode[]; parent: Inode; size: number; + priority?: number; type = FileSystemType.InodeType; public static VALID_NAME_REGEX = /^[-\w^&'@{}[\],$=!#():.%+~ ]+$/; @@ -20,8 +22,10 @@ export default class Inode extends TreeNode implements SerializableNode { if(!options.skipNameValidation) { this.validateName(); } + this.index = options?.index; this.progress = options.progress || 1; // Assume file is fully downloaded otherwise this.size = options.size || 0; + this.priority = options.priority || 1; } /** Download progress of a file/folder as fraction between 0 and 1. @@ -145,6 +149,8 @@ export default class Inode extends TreeNode implements SerializableNode { export interface InodeConstructor extends TreeNodeConstructor{ children?: Inode[], progress?: number, + priority?: number, + index?: any, /** Whether to skip name validation or not. * DANGEROUS -- USE WITH CAUTION! */ diff --git a/src/app/services/file-system/file-system.service.ts b/src/app/services/file-system/file-system.service.ts index 413adc8d..3cadb2f6 100644 --- a/src/app/services/file-system/file-system.service.ts +++ b/src/app/services/file-system/file-system.service.ts @@ -91,8 +91,8 @@ export class FileSystemService { let newDirNode: any; // If a folder, create directory type - if(dir === lastElement && data.type === "File") { newDirNode = new FileNode({value: dir, children: null, size: data.size, progress: data.progress}); } - else { newDirNode = new DirectoryNode({value: dir}); } + if(dir === lastElement && data.type === "File") { newDirNode = new FileNode({index: data.index, value: dir, children: null, size: data.size, progress: data.progress, priority: data.priority}); } + else { newDirNode = new DirectoryNode({index: data.index, value: dir, priority: data.priority}); } curr.addChildNode(newDirNode); curr = newDirNode; @@ -136,12 +136,14 @@ export class FileSystemService { if(node.hasChildren()) { for(const child of node.getChildren()) { result.push({ + index: child.index, name: child.getValue(), path: child.getAbsolutePath(this.directoryDelimiter), parentPath: node.getAbsolutePath(this.directoryDelimiter), children: await this._convertToJSON(child), size: child.getSize(), progress: child.getProgressAmount(), + priority: child.priority, type: child.type }); } @@ -192,11 +194,13 @@ export class FileSystemService { } export interface SerializedNode { + index?: any, name: string, path: string, parentPath: string, size: number, progress?: number, + priority?: number, type?: 'File' | 'Directory', children?: SerializedNode[] } diff --git a/src/app/services/torrent-management/torrent-data-http.service.ts b/src/app/services/torrent-management/torrent-data-http.service.ts index 680aa286..f8035f12 100644 --- a/src/app/services/torrent-management/torrent-data-http.service.ts +++ b/src/app/services/torrent-management/torrent-data-http.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { MainData, QbittorrentBuildInfo, TorrentContents, UserPreferences } from 'src/utils/Interfaces'; +import { MainData, QbittorrentBuildInfo, Torrent, TorrentContents, UserPreferences } from 'src/utils/Interfaces'; import { Observable } from 'rxjs'; // Utils @@ -189,6 +189,19 @@ export class TorrentDataHTTPService { return this.sendTorrentHashesPOST(url, hashes); } + SetFilePriority(tor: Torrent, indexes: any[], priority: number): Observable { + let root = this.http_endpoints.root; + let endpoint = this.http_endpoints.filePrio; + let url = root + endpoint; + + let body = new FormData(); + body.append('hash', tor.hash); + body.append('id', indexes.join('|')); + body.append('priority', priority.toString()); + + return this.sendTorrentHashesPOST(url, null, null, body) + } + SetPreferences(pref: UserPreferences): Observable { let root = this.http_endpoints.root; let endpoint = this.http_endpoints.setPreferences; diff --git a/src/app/services/torrent-management/torrent-data-store.service.ts b/src/app/services/torrent-management/torrent-data-store.service.ts index 776a7cd1..30e6a1c5 100644 --- a/src/app/services/torrent-management/torrent-data-store.service.ts +++ b/src/app/services/torrent-management/torrent-data-store.service.ts @@ -131,6 +131,10 @@ export class TorrentDataStoreService { return this.torrent_http_service.SetMinimumPriority(tor.map(elem => elem.hash)); } + public SetFilePriority(tor: Torrent, indexes: any[], priority: number): Observable { + return this.torrent_http_service.SetFilePriority(tor, indexes, priority); + } + public SetUserPreferences(pref: UserPreferences): Observable { return this.torrent_http_service.SetPreferences(pref); } @@ -204,6 +208,9 @@ export class TorrentDataStoreService { if(new Date(tor.completion_on * 1000) < TorrentDataStoreService.CREATED_AT_THRESHOLD) { tor.completion_on = TorrentDataStoreService.FUTURE_MOST_DATE.valueOf() / 1000 } + + // Ensure progress is between 0 and 1 + tor.progress = Math.max(0, Math.min(1, tor.progress)); } /** Update server status in changelog */ diff --git a/src/app/services/units-helper.service.ts b/src/app/services/units-helper.service.ts index b437f4c6..f726e557 100644 --- a/src/app/services/units-helper.service.ts +++ b/src/app/services/units-helper.service.ts @@ -12,6 +12,9 @@ export class UnitsHelperService { * e.g. 1,572,864 -> 1.57 MB */ public GetFileSizeString(size: number): string { + if(size < 0) return '∞'; + if(size === undefined || size == null) return 'Unknown'; + const DP = 2; // Number of fixed decimal places for rounding const B = 1; @@ -47,6 +50,7 @@ export class UnitsHelperService { return TB_s; } + console.error('File size too large to convert:', size); return "ERROR -- Too large"; } @@ -102,9 +106,10 @@ export class UnitsHelperService { */ public GetDateString(timestamp: number): string { let date = new Date(timestamp * 1000); + let am_pm = date.getHours() > 11 ? 'PM' : 'AM'; let result = `${this.getDay(date)}/${this.getMonth(date)}/${date.getFullYear()}, - ${this.getHours(date)}:${this.getMinutes(date)}:${this.getSeconds(date)}`; + ${this.getHours(date)}:${this.getMinutes(date)}:${this.getSeconds(date)} ${am_pm}`; return result; } @@ -131,6 +136,6 @@ export class UnitsHelperService { } private getHours(date: Date): string { - return (date.getHours()) < 10 ? `0${date.getHours()}` : `${date.getHours()}` + return (date.getHours() % 12) < 10 ? `0${date.getHours() % 12}` : `${date.getHours() % 12}` } } diff --git a/src/app/torrents-table/torrents-table.component.html b/src/app/torrents-table/torrents-table.component.html index 360b6688..afab905e 100644 --- a/src/app/torrents-table/torrents-table.component.html +++ b/src/app/torrents-table/torrents-table.component.html @@ -135,7 +135,7 @@ diff --git a/src/app/torrents-table/torrents-table.component.scss b/src/app/torrents-table/torrents-table.component.scss index 897ff00a..c322d91f 100644 --- a/src/app/torrents-table/torrents-table.component.scss +++ b/src/app/torrents-table/torrents-table.component.scss @@ -1,4 +1,3 @@ -@import "~@angular/material/theming"; @import "../../variables.scss"; .hidden { @@ -81,68 +80,22 @@ mat-spinner { &.table-col-select { width: 50px; } &.table-col-Actions { width: 140px; } &.table-col-Size { width: 85px; } - &.table-col-Progress { width: 200px; } + &.table-col-Progress { width: 175px; } &.table-col-Status { width: 140px; } &.table-col-Up-Speed { width: 100px; } &.table-col-Down-Speed { width: 120px; } - &.table-col-ETA { width: 125px; } + &.table-col-ETA { width: 115px; } &.table-col-Ratio { width: 75px; } &.table-col-Uploaded { width: 85px; } &.table-col-Added-On { width: 150px; } - &.table-col-Completed-On { width: 150px; } - &.table-col-Last-Activity { width: 150px; } + &.table-col-Completed-On { width: 170px; } + &.table-col-Last-Activity { width: 170px; } } i.pi { font-size: 15px; } -.torrent-table-status { - color: white; - - &.primary { - background: mat-color($my-app-primary) - } - - &.warning { - background: mat-color($my-app-accent) - } - - &.danger { - background: mat-color($my-app-warn) - } - - &.info { - background: $my-app-default; - color: $table-black-color; - } -} - -// For dark mode! -.dark-theme { - .torrent-table-status { - &.primary { - background: mat-color($dark-primary); - color: $table-black-color; - } - - &.warning { - background: mat-color($dark-accent); - color: $table-black-color; - } - - &.danger { - background: mat-color($dark-warn); - } - - &.info { - background: $dark-default; - color: white; - } - } -} - - /** Torrent table header and row styles **/ .p-datatable .p-datatable-thead > tr > th { diff --git a/src/app/torrents-table/torrents-table.component.ts b/src/app/torrents-table/torrents-table.component.ts index a7feb438..51319686 100644 --- a/src/app/torrents-table/torrents-table.component.ts +++ b/src/app/torrents-table/torrents-table.component.ts @@ -22,6 +22,7 @@ import { ApplicationConfigService } from '../services/app/application-config.ser import { TorrentHelperService } from '../services/torrent-management/torrent-helper.service'; import { SnackbarService } from '../services/notifications/snackbar.service'; import { MenuItem } from 'primeng/api'; +import { getClassForStatus } from '../../utils/Helpers' @Component({ @@ -336,27 +337,7 @@ export class TorrentsTableComponent implements OnInit { this.handleSortChange(this.currentMatSort); } - public isTorrentPrimaryAction(tor: Torrent): boolean { - return tor.state === 'downloading'; - } - - /** Determine if torrent is in a error state */ - public isTorrentError(tor: Torrent): boolean { - let errors = ['missingFiles', 'error', 'unknown']; - return errors.includes(tor.state); - } - - public isTorrentWarning(tor: Torrent): boolean { - let warnings = ['moving', 'checkingDL']; - return warnings.includes(tor.state); - } - - public getClassForStatus(torrent: Torrent): string { - let root = 'torrent-table-status ' - let suffix = this.isTorrentPrimaryAction(torrent) ? 'primary' : this.isTorrentError(torrent) ? 'danger' : this.isTorrentWarning(torrent) ? 'warning' : 'info' - - return root + suffix; - } + public getClassForStatus(t: Torrent): string { return getClassForStatus(t); } public isTorrentsEmpty() { return this.filteredTorrentData?.length === 0; diff --git a/src/assets/http_config.json b/src/assets/http_config.json index 09718686..1aad4eef 100644 --- a/src/assets/http_config.json +++ b/src/assets/http_config.json @@ -14,6 +14,7 @@ "decreasePrio": "/torrents/decreasePrio", "maxPrio": "/torrents/topPrio", "minPrio": "/torrents/bottomPrio", + "filePrio": "/torrents/filePrio", "moveTorrents": "/torrents/setLocation", "torrentContents": "/torrents/files", "userPreferences": "/app/preferences", diff --git a/src/assets/http_config.prod.json b/src/assets/http_config.prod.json index 1757685e..1ec3e602 100644 --- a/src/assets/http_config.prod.json +++ b/src/assets/http_config.prod.json @@ -14,6 +14,7 @@ "decreasePrio": "/torrents/decreasePrio", "maxPrio": "/torrents/topPrio", "minPrio": "/torrents/bottomPrio", + "filePrio": "/torrents/filePrio", "moveTorrents": "/torrents/setLocation", "torrentContents": "/torrents/files", "userPreferences": "/app/preferences", diff --git a/src/styles.scss b/src/styles.scss index c62cde11..066c49ee 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,5 +1,7 @@ /* You can add global styles to this file, and also import other style files */ @use 'style/fonts.scss'; +@import "~@angular/material/theming"; +@import "variables.scss"; html, body { height: 100%; min-width: 800px;} body { @@ -126,9 +128,54 @@ body { .snackbar-error { background: #F44336; } .snackbar-info { background: #2196F3; } +.torrent-status { + color: white; + + &.primary { + background: mat-color($my-app-primary) + } + + &.warning { + background: mat-color($my-app-accent) + } + + &.danger { + background: mat-color($my-app-warn) + } + + &.info { + background: $my-app-default; + color: $table-black-color; + } +} + +// For dark mode! +.dark-theme { + .torrent-status { + &.primary { + background: mat-color($dark-primary); + color: $table-black-color; + } + + &.warning { + background: mat-color($dark-accent); + color: $table-black-color; + } + + &.danger { + background: mat-color($dark-warn); + } + + &.info { + background: $dark-default; + color: white; + } + } +} + /*** CUSTOM SCROLLBAR ***/ ::-webkit-scrollbar { - width: 5px; /** Target vertical scroll-bars */ + width: 8px; /** Target vertical scroll-bars */ height: 8px; /** Target horizontal scroll-bars */ } diff --git a/src/utils/Helpers.ts b/src/utils/Helpers.ts index 208987f4..ec6f0443 100644 --- a/src/utils/Helpers.ts +++ b/src/utils/Helpers.ts @@ -1,3 +1,5 @@ +import { Torrent } from "./Interfaces"; + /** Get more easily comparable name for a torrent * Commonly, torrents will substitute a "." period for a space. * @@ -23,6 +25,28 @@ export function GetDefaultSaveLocation(): string { return save_location || ""; } +function isTorrentPrimaryAction(tor: Torrent): boolean { + return tor.state === 'downloading'; +} + +/** Determine if torrent is in a error state */ +function isTorrentError(tor: Torrent): boolean { + let errors = ['missingFiles', 'error', 'unknown']; + return errors.includes(tor.state); +} + +function isTorrentWarning(tor: Torrent): boolean { + let warnings = ['moving', 'checkingDL']; + return warnings.includes(tor.state); +} + +export function getClassForStatus(torrent: Torrent): string { + let root = 'torrent-status ' + let suffix = isTorrentPrimaryAction(torrent) ? 'primary' : isTorrentError(torrent) ? 'danger' : isTorrentWarning(torrent) ? 'warning' : 'info' + + return root + suffix; +} + /** * Simple object check. diff --git a/src/utils/Interfaces.ts b/src/utils/Interfaces.ts index f6285bf8..52e3f235 100644 --- a/src/utils/Interfaces.ts +++ b/src/utils/Interfaces.ts @@ -21,7 +21,11 @@ export interface Torrent { uploaded: number, progress: number, dlspeed: number, + dl_speed_avg: number, upspeed: number, + up_speed_avg: number, + up_limit: number, + dl_limit: number, priority: number, num_seeds: number, num_leechs: number, @@ -93,6 +97,7 @@ export interface MainData { /** The contents of a torrent */ export interface TorrentContents { + index: any, /** File name (including relative path) */ name: string, /** File size (bytes) */