feat: expandable error details with copy-to-clipboard (closes #143)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7427cbb0c0
commit
0104add945
2 changed files with 51 additions and 9 deletions
|
|
@ -456,20 +456,42 @@
|
||||||
<fa-icon [icon]="faCheckCircle" class="text-success" />
|
<fa-icon [icon]="faCheckCircle" class="text-success" />
|
||||||
}
|
}
|
||||||
@if (download.value.status === 'error') {
|
@if (download.value.status === 'error') {
|
||||||
<fa-icon [icon]="faTimesCircle" class="text-danger" />
|
<fa-icon [icon]="faTimesCircle" class="text-danger" style="cursor: pointer;" (click)="toggleErrorDetail(download.key)" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<span ngbTooltip="{{buildResultItemTooltip(download.value)}}">@if (!!download.value.filename) {
|
<span ngbTooltip="{{buildResultItemTooltip(download.value)}}">@if (!!download.value.filename) {
|
||||||
<a href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a>
|
<a href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a>
|
||||||
} @else {
|
} @else {
|
||||||
{{download.value.title}}
|
<span [style.cursor]="download.value.status === 'error' ? 'pointer' : 'default'"
|
||||||
@if (download.value.msg) {
|
(click)="download.value.status === 'error' ? toggleErrorDetail(download.key) : null">
|
||||||
<span><br>{{download.value.msg}}</span>
|
{{download.value.title}}
|
||||||
}
|
@if (download.value.status === 'error' && !isErrorExpanded(download.key)) {
|
||||||
@if (download.value.error) {
|
<small class="text-danger ms-2">
|
||||||
<span><br>Error: {{download.value.error}}</span>
|
<fa-icon [icon]="faChevronRight" size="xs" class="me-1" />Click for details
|
||||||
}
|
</small>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
}</span>
|
}</span>
|
||||||
|
@if (download.value.status === 'error' && isErrorExpanded(download.key)) {
|
||||||
|
<div class="alert alert-danger py-2 px-3 mt-2 mb-0 small" style="border-left: 4px solid var(--bs-danger);">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
@if (download.value.msg) {
|
||||||
|
<div class="mb-1"><strong>Message:</strong> {{download.value.msg}}</div>
|
||||||
|
}
|
||||||
|
@if (download.value.error) {
|
||||||
|
<div class="mb-1"><strong>Error:</strong> {{download.value.error}}</div>
|
||||||
|
}
|
||||||
|
<div class="text-muted" style="word-break: break-all;"><strong>URL:</strong> {{download.value.url}}</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger ms-2 flex-shrink-0"
|
||||||
|
(click)="copyErrorMessage(download.value); $event.stopPropagation()"
|
||||||
|
ngbTooltip="Copy error details to clipboard">
|
||||||
|
<fa-icon [icon]="faCopy" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (download.value.size) {
|
@if (download.value.size) {
|
||||||
|
|
|
||||||
|
|
@ -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, faChevronRight } 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;
|
||||||
|
expandedErrors: Set<string> = new Set();
|
||||||
|
|
||||||
// Download metrics
|
// Download metrics
|
||||||
activeDownloads = 0;
|
activeDownloads = 0;
|
||||||
|
|
@ -100,6 +101,7 @@ export class App implements AfterViewInit, OnInit {
|
||||||
faGithub = faGithub;
|
faGithub = faGithub;
|
||||||
faClock = faClock;
|
faClock = faClock;
|
||||||
faTachometerAlt = faTachometerAlt;
|
faTachometerAlt = faTachometerAlt;
|
||||||
|
faChevronRight = faChevronRight;
|
||||||
subtitleFormats = [
|
subtitleFormats = [
|
||||||
{ id: 'srt', text: 'SRT' },
|
{ id: 'srt', text: 'SRT' },
|
||||||
{ id: 'txt', text: 'TXT (Text only)' },
|
{ id: 'txt', text: 'TXT (Text only)' },
|
||||||
|
|
@ -699,6 +701,24 @@ export class App implements AfterViewInit, OnInit {
|
||||||
this.isAdvancedOpen = !this.isAdvancedOpen;
|
this.isAdvancedOpen = !this.isAdvancedOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleErrorDetail(id: string) {
|
||||||
|
if (this.expandedErrors.has(id)) this.expandedErrors.delete(id);
|
||||||
|
else this.expandedErrors.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyErrorMessage(download: Download) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (download.title) parts.push(`Title: ${download.title}`);
|
||||||
|
if (download.url) parts.push(`URL: ${download.url}`);
|
||||||
|
if (download.msg) parts.push(`Message: ${download.msg}`);
|
||||||
|
if (download.error) parts.push(`Error: ${download.error}`);
|
||||||
|
navigator.clipboard.writeText(parts.join('\n')).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
isErrorExpanded(id: string): boolean {
|
||||||
|
return this.expandedErrors.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue