import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt, faSun, faMoon, faExternalLinkAlt, faDownload } from '@fortawesome/free-solid-svg-icons'; import { CookieService } from 'ngx-cookie-service'; import { map, Observable, of } from 'rxjs'; import { Download, DownloadsService, Status } from './downloads.service'; import { MasterCheckboxComponent } from './master-checkbox.component'; import { Formats, Format, Quality } from './formats'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.sass'], }) export class AppComponent implements AfterViewInit { addUrl: string; formats: Format[] = Formats; qualities: Quality[]; quality: string; format: string; folder: string; addInProgress = false; darkMode: boolean; customDirs$: Observable; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @ViewChild('queueDelSelected') queueDelSelected: ElementRef; @ViewChild('doneMasterCheckbox') doneMasterCheckbox: MasterCheckboxComponent; @ViewChild('doneDelSelected') doneDelSelected: ElementRef; @ViewChild('doneClearCompleted') doneClearCompleted: ElementRef; @ViewChild('doneClearFailed') doneClearFailed: ElementRef; faTrashAlt = faTrashAlt; faCheckCircle = faCheckCircle; faTimesCircle = faTimesCircle; faRedoAlt = faRedoAlt; faSun = faSun; faMoon = faMoon; faDownload = faDownload; faExternalLinkAlt = faExternalLinkAlt; constructor(public downloads: DownloadsService, private cookieService: CookieService) { this.format = cookieService.get('metube_format') || 'any'; // Needs to be set or qualities won't automatically be set this.setQualities() this.quality = cookieService.get('metube_quality') || 'best'; this.setupTheme(cookieService) } ngOnInit() { this.customDirs$ = this.getMatchingCustomDir(); } ngAfterViewInit() { this.downloads.queueChanged.subscribe(() => { this.queueMasterCheckbox.selectionChanged(); }); this.downloads.doneChanged.subscribe(() => { this.doneMasterCheckbox.selectionChanged(); let completed: number = 0, failed: number = 0; this.downloads.done.forEach(dl => { if (dl.status === 'finished') completed++; else if (dl.status === 'error') failed++; }); this.doneClearCompleted.nativeElement.disabled = completed === 0; this.doneClearFailed.nativeElement.disabled = failed === 0; }); } // workaround to allow fetching of Map values in the order they were inserted // https://github.com/angular/angular/issues/31420 asIsOrder(a, b) { return 1; } qualityChanged() { this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); // Re-trigger custom directory change this.downloads.customDirsChanged.next(this.downloads.customDirs); } showAdvanced() { return this.downloads.configuration['CUSTOM_DIRS']; } allowCustomDir(tag: string) { if (this.downloads.configuration['CREATE_CUSTOM_DIRS']) { return tag; } return false; } isAudioType() { return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a'; } getMatchingCustomDir() : Observable { return this.downloads.customDirsChanged.asObservable().pipe(map((output) => { // Keep logic consistent with app/ytdl.py if (this.isAudioType()) { console.debug("Showing audio-specific download directories"); return output["audio_download_dir"]; } else { console.debug("Showing default download directories"); return output["download_dir"]; } })); } setupTheme(cookieService) { if (cookieService.check('metube_dark')) { this.darkMode = cookieService.get('metube_dark') === "true" } else { this.darkMode = window.matchMedia("prefers-color-scheme: dark").matches } this.setTheme() } themeChanged() { this.darkMode = !this.darkMode this.cookieService.set('metube_dark', this.darkMode.toString(), { expires: 3650 }); this.setTheme() } setTheme() { const doc = document.querySelector('html') const filter = this.darkMode ? "invert(1) hue-rotate(180deg)" : "" doc.style.filter = filter } formatChanged() { this.cookieService.set('metube_format', this.format, { expires: 3650 }); // Updates to use qualities available this.setQualities() // Re-trigger custom directory change this.downloads.customDirsChanged.next(this.downloads.customDirs); } queueSelectionChanged(checked: number) { this.queueDelSelected.nativeElement.disabled = checked == 0; } doneSelectionChanged(checked: number) { this.doneDelSelected.nativeElement.disabled = checked == 0; } setQualities() { // qualities for specific format this.qualities = this.formats.find(el => el.id == this.format).qualities const exists = this.qualities.find(el => el.id === this.quality) this.quality = exists ? this.quality : 'best' } addDownload(url?: string, quality?: string, format?: string, folder?: string) { url = url ?? this.addUrl quality = quality ?? this.quality format = format ?? this.format folder = folder ?? this.folder console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder); this.addInProgress = true; this.downloads.add(url, quality, format, folder).subscribe((status: Status) => { if (status.status === 'error') { alert(`Error adding URL: ${status.msg}`); } else { this.addUrl = ''; } this.addInProgress = false; }); } retryDownload(key: string, url: string, quality: string, format: string, folder: string) { this.addDownload(url, quality, format, folder); this.downloads.delById('done', [key]).subscribe(); } delDownload(where: string, id: string) { this.downloads.delById(where, [id]).subscribe(); } delSelectedDownloads(where: string) { this.downloads.delByFilter(where, dl => dl.checked).subscribe(); } clearCompletedDownloads() { this.downloads.delByFilter('done', dl => dl.status === 'finished').subscribe(); } clearFailedDownloads() { this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe(); } buildDownloadLink(download: Download) { let baseDir = 'download/'; if (download.quality == 'audio' || download.filename.endsWith('.mp3')) { baseDir = 'audio_download/'; } if (download.folder) { baseDir += download.folder + '/'; } return baseDir + encodeURIComponent(download.filename); } }