+ +
+
+
+
+ + Recent Events +
+ +
+
+
+
+
+ +
+
+
{{ event.message }}
+
+ {{ event.url }} +
+
+
+ {{ getRelativeTime(event.timestamp) }} +
+
+
+
+
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index dceab1d..6e590f9 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -236,3 +236,86 @@ main fa-icon vertical-align: middle + +// Events display area +.events-container + background: var(--bs-body-bg) + border: 1px solid var(--bs-border-color) + border-radius: 0.5rem + overflow: hidden + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) + margin-top: 1rem + + .events-header + padding: 0.75rem 1rem + background: var(--bs-secondary-bg) + border-bottom: 1px solid var(--bs-border-color) + + h6 + color: var(--bs-secondary-color) + font-weight: 600 + display: flex + align-items: center + + fa-icon + opacity: 0.7 + + .events-list + max-height: 300px + overflow-y: auto + + .event-item + display: flex + align-items: flex-start + padding: 0.875rem 1rem + border-bottom: 1px solid var(--bs-border-color) + transition: background-color 0.2s + + &:last-child + border-bottom: none + + &:hover + background: var(--bs-tertiary-bg) + + .event-icon + flex-shrink: 0 + width: 32px + height: 32px + display: flex + align-items: center + justify-content: center + border-radius: 50% + margin-right: 0.75rem + + fa-icon + font-size: 1.1rem + + &.event-duplicate_skipped + .event-icon + background: rgba(255, 193, 7, 0.15) + color: #ffc107 + + .event-content + flex: 1 + min-width: 0 + + .event-message + font-size: 0.9375rem + color: var(--bs-body-color) + margin-bottom: 0.25rem + font-weight: 500 + + .event-url + font-size: 0.8125rem + color: var(--bs-secondary-color) + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + font-family: 'Courier New', monospace + + .event-time + flex-shrink: 0 + font-size: 0.75rem + color: var(--bs-secondary-color) + margin-left: 0.75rem + whitespace: nowrap diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 837d087..dcb75a5 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt } from '@fortawesome/free-solid-svg-icons'; @@ -18,7 +18,7 @@ import {KeyValue} from "@angular/common"; styleUrls: ['./app.component.sass'], standalone: false }) -export class AppComponent implements AfterViewInit { +export class AppComponent implements OnInit, AfterViewInit { addUrl: string; formats: Format[] = Formats; qualities: Quality[]; @@ -78,6 +78,9 @@ export class AppComponent implements AfterViewInit { faClock = faClock; faTachometerAlt = faTachometerAlt; + // Events from backend (last 5 events) + events: Array<{type: string, message: string, timestamp: number, url: string}> = []; + constructor(public downloads: DownloadsService, private cookieService: CookieService, private http: HttpClient) { this.format = cookieService.get('metube_format') || 'any'; // Needs to be set or qualities won't automatically be set @@ -98,6 +101,15 @@ export class AppComponent implements AfterViewInit { this.downloads.updated.subscribe(() => { this.updateMetrics(); }); + // Subscribe to events + this.downloads.eventReceived.subscribe((event: any) => { + console.debug('Event received in component:', event); + // Add to events array (keep last 5) + this.events.push(event); + if (this.events.length > 5) { + this.events = this.events.slice(-5); + } + }); } ngOnInit() { @@ -106,6 +118,9 @@ export class AppComponent implements AfterViewInit { this.customDirs$ = this.getMatchingCustomDir(); this.setTheme(this.activeTheme); + // Load events from backend + this.loadEvents(); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (this.activeTheme.id === 'auto') { this.setTheme(this.activeTheme); @@ -269,11 +284,38 @@ export class AppComponent implements AfterViewInit { alert(`Error adding URL: ${status.msg}`); } else { this.addUrl = ''; + // Reload events after adding + this.loadEvents(); } this.addInProgress = false; }); } + // Load events from backend + loadEvents() { + this.http.get('/events').subscribe((events: any[]) => { + this.events = events || []; + }); + } + + // Clear all events + clearEvents() { + this.http.post('/events/clear', {}).subscribe(() => { + this.events = []; + }); + } + + // Format timestamp to relative time + getRelativeTime(timestamp: number): string { + const now = Date.now() / 1000; + const diff = Math.floor(now - timestamp); + + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; + } + downloadItemByKey(id: string) { this.downloads.startById([id]).subscribe(); } diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 2bac988..d6c5686 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -44,6 +44,7 @@ export class DownloadsService { ytdlOptionsChanged = new Subject(); configurationChanged = new Subject(); updated = new Subject(); + eventReceived = new Subject(); // New subject for events configuration = {}; customDirs = {}; @@ -112,6 +113,11 @@ export class DownloadsService { let data = JSON.parse(strdata); this.ytdlOptionsChanged.next(data); }); + socket.fromEvent('event').subscribe((strdata: string) => { + let event = JSON.parse(strdata); + console.debug('Received event:', event); + this.eventReceived.next(event); + }); } handleHTTPError(error: HttpErrorResponse) {