mirror of
https://github.com/alexta69/metube.git
synced 2026-03-18 22:43:51 +00:00
feat: sort completed downloads by newest first (closes #610)
This commit is contained in:
@@ -424,7 +424,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metube-section-header">Completed</div>
|
<div class="metube-section-header d-flex align-items-center">
|
||||||
|
Completed
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary ms-3"
|
||||||
|
(click)="toggleSortOrder()"
|
||||||
|
ngbTooltip="{{ sortAscending ? 'Oldest first' : 'Newest first' }}">
|
||||||
|
<fa-icon [icon]="sortAscending ? faSortAmountUp : faSortAmountDown" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="px-2 py-3 border-bottom">
|
<div class="px-2 py-3 border-bottom">
|
||||||
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt" /> Clear selected</button>
|
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt" /> Clear selected</button>
|
||||||
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle" /> Clear completed</button>
|
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle" /> Clear completed</button>
|
||||||
@@ -445,58 +452,58 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (download of downloads.done | keyvalue: asIsOrder; track download.value.id) {
|
@for (entry of sortedDone(); track entry[1].id) {
|
||||||
<tr [class.disabled]='download.value.deleting'>
|
<tr [class.disabled]='entry[1].deleting'>
|
||||||
<td>
|
<td>
|
||||||
<app-slave-checkbox [id]="download.key" [master]="doneMasterCheckboxRef" [checkable]="download.value" />
|
<app-slave-checkbox [id]="entry[0]" [master]="doneMasterCheckboxRef" [checkable]="entry[1]" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display: inline-block; width: 1.5rem;">
|
<div style="display: inline-block; width: 1.5rem;">
|
||||||
@if (download.value.status === 'finished') {
|
@if (entry[1].status === 'finished') {
|
||||||
<fa-icon [icon]="faCheckCircle" class="text-success" />
|
<fa-icon [icon]="faCheckCircle" class="text-success" />
|
||||||
}
|
}
|
||||||
@if (download.value.status === 'error') {
|
@if (entry[1].status === 'error') {
|
||||||
<fa-icon [icon]="faTimesCircle" class="text-danger" />
|
<fa-icon [icon]="faTimesCircle" class="text-danger" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<span ngbTooltip="{{buildResultItemTooltip(download.value)}}">@if (!!download.value.filename) {
|
<span ngbTooltip="{{buildResultItemTooltip(entry[1])}}">@if (!!entry[1].filename) {
|
||||||
<a href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a>
|
<a href="{{buildDownloadLink(entry[1])}}" target="_blank">{{ entry[1].title }}</a>
|
||||||
} @else {
|
} @else {
|
||||||
{{download.value.title}}
|
{{entry[1].title}}
|
||||||
@if (download.value.msg) {
|
@if (entry[1].msg) {
|
||||||
<span><br>{{download.value.msg}}</span>
|
<span><br>{{entry[1].msg}}</span>
|
||||||
}
|
}
|
||||||
@if (download.value.error) {
|
@if (entry[1].error) {
|
||||||
<span><br>Error: {{download.value.error}}</span>
|
<span><br>Error: {{entry[1].error}}</span>
|
||||||
}
|
}
|
||||||
}</span>
|
}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (download.value.size) {
|
@if (entry[1].size) {
|
||||||
<span>{{ download.value.size | fileSize }}</span>
|
<span>{{ entry[1].size | fileSize }}</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@if (download.value.status === 'error') {
|
@if (entry[1].status === 'error') {
|
||||||
<button type="button" class="btn btn-link" (click)="retryDownload(download.key, download.value)"><fa-icon [icon]="faRedoAlt" /></button>
|
<button type="button" class="btn btn-link" (click)="retryDownload(entry[0], entry[1])"><fa-icon [icon]="faRedoAlt" /></button>
|
||||||
}
|
}
|
||||||
@if (download.value.filename) {
|
@if (entry[1].filename) {
|
||||||
<a href="{{buildDownloadLink(download.value)}}" download class="btn btn-link"><fa-icon [icon]="faDownload" /></a>
|
<a href="{{buildDownloadLink(entry[1])}}" download class="btn btn-link"><fa-icon [icon]="faDownload" /></a>
|
||||||
}
|
}
|
||||||
<a href="{{download.value.url}}" target="_blank" class="btn btn-link"><fa-icon [icon]="faExternalLinkAlt" /></a>
|
<a href="{{entry[1].url}}" target="_blank" class="btn btn-link"><fa-icon [icon]="faExternalLinkAlt" /></a>
|
||||||
<button type="button" class="btn btn-link" (click)="delDownload('done', download.key)"><fa-icon [icon]="faTrashAlt" /></button>
|
<button type="button" class="btn btn-link" (click)="delDownload('done', entry[0])"><fa-icon [icon]="faTrashAlt" /></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@if (download.value.chapter_files && download.value.chapter_files.length > 0) {
|
@if (entry[1].chapter_files && entry[1].chapter_files.length > 0) {
|
||||||
@for (chapterFile of download.value.chapter_files; track chapterFile.filename) {
|
@for (chapterFile of entry[1].chapter_files; track chapterFile.filename) {
|
||||||
<tr [class.disabled]='download.value.deleting'>
|
<tr [class.disabled]='entry[1].deleting'>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<div style="padding-left: 2rem;">
|
<div style="padding-left: 2rem;">
|
||||||
<fa-icon [icon]="faCheckCircle" class="text-success me-2" />
|
<fa-icon [icon]="faCheckCircle" class="text-success me-2" />
|
||||||
<a href="{{buildChapterDownloadLink(download.value, chapterFile.filename)}}" target="_blank">{{
|
<a href="{{buildChapterDownloadLink(entry[1], chapterFile.filename)}}" target="_blank">{{
|
||||||
getChapterFileName(chapterFile.filename) }}</a>
|
getChapterFileName(chapterFile.filename) }}</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -507,7 +514,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a href="{{buildChapterDownloadLink(download.value, chapterFile.filename)}}" download
|
<a href="{{buildChapterDownloadLink(entry[1], chapterFile.filename)}}" download
|
||||||
class="btn btn-link"><fa-icon [icon]="faDownload" /></a>
|
class="btn btn-link"><fa-icon [icon]="faDownload" /></a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { DownloadsService } from './services/downloads.service';
|
import { DownloadsService } from './services/downloads.service';
|
||||||
@@ -66,6 +66,7 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
ytDlpVersion: string | null = null;
|
ytDlpVersion: string | null = null;
|
||||||
metubeVersion: string | null = null;
|
metubeVersion: string | null = null;
|
||||||
isAdvancedOpen = false;
|
isAdvancedOpen = false;
|
||||||
|
sortAscending = false;
|
||||||
|
|
||||||
// Download metrics
|
// Download metrics
|
||||||
activeDownloads = 0;
|
activeDownloads = 0;
|
||||||
@@ -100,6 +101,8 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
faGithub = faGithub;
|
faGithub = faGithub;
|
||||||
faClock = faClock;
|
faClock = faClock;
|
||||||
faTachometerAlt = faTachometerAlt;
|
faTachometerAlt = faTachometerAlt;
|
||||||
|
faSortAmountDown = faSortAmountDown;
|
||||||
|
faSortAmountUp = faSortAmountUp;
|
||||||
subtitleFormats = [
|
subtitleFormats = [
|
||||||
{ id: 'srt', text: 'SRT' },
|
{ id: 'srt', text: 'SRT' },
|
||||||
{ id: 'txt', text: 'TXT (Text only)' },
|
{ id: 'txt', text: 'TXT (Text only)' },
|
||||||
@@ -178,6 +181,7 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
if (!allowedSubtitleModes.has(this.subtitleMode)) {
|
if (!allowedSubtitleModes.has(this.subtitleMode)) {
|
||||||
this.subtitleMode = 'prefer_manual';
|
this.subtitleMode = 'prefer_manual';
|
||||||
}
|
}
|
||||||
|
this.sortAscending = this.cookieService.get('metube_sort_ascending') === 'true';
|
||||||
|
|
||||||
this.activeTheme = this.getPreferredTheme(this.cookieService);
|
this.activeTheme = this.getPreferredTheme(this.cookieService);
|
||||||
|
|
||||||
@@ -699,6 +703,25 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
this.isAdvancedOpen = !this.isAdvancedOpen;
|
this.isAdvancedOpen = !this.isAdvancedOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSortOrder() {
|
||||||
|
this.sortAscending = !this.sortAscending;
|
||||||
|
this.cookieService.set('metube_sort_ascending', this.sortAscending ? 'true' : 'false', { expires: 3650 });
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedDone(): [string, Download][] {
|
||||||
|
const result: [string, Download][] = [];
|
||||||
|
this.downloads.done.forEach((dl, key) => {
|
||||||
|
result.push([key, dl]);
|
||||||
|
});
|
||||||
|
result.sort((a, b) => {
|
||||||
|
const tsA = (a[1] as any).timestamp || 0;
|
||||||
|
const tsB = (b[1] as any).timestamp || 0;
|
||||||
|
const cmp = tsA < tsB ? -1 : tsA > tsB ? 1 : 0;
|
||||||
|
return this.sortAscending ? cmp : -cmp;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private updateMetrics() {
|
private updateMetrics() {
|
||||||
this.activeDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading' || d.status === 'preparing').length;
|
this.activeDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading' || d.status === 'preparing').length;
|
||||||
this.queuedDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'pending').length;
|
this.queuedDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'pending').length;
|
||||||
|
|||||||
Reference in New Issue
Block a user