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:
ddmoney420 2026-02-28 22:11:35 -07:00
parent 7427cbb0c0
commit 0104add945
2 changed files with 51 additions and 9 deletions

View file

@ -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) {

View file

@ -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;