Merge branch 'pr-977'

This commit is contained in:
Alex Shnitman
2026-05-29 14:14:05 +03:00
4 changed files with 85 additions and 99 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"build:watch": "ng build --watch", "build:watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"lint": "ng lint" "lint": "ng lint"
}, },
+49 -63
View File
@@ -279,13 +279,12 @@
</div> </div>
<div class="col-12 col-md-6 col-lg-3"> <div class="col-12 col-md-6 col-lg-3">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Format</span> <span class="input-group-text help-title" ngbPopover="Subtitle output format for captions mode." triggers="click" autoClose="outside" container="body">Format</span>
<select class="form-select" <select class="form-select"
name="format" name="format"
[(ngModel)]="format" [(ngModel)]="format"
(change)="formatChanged()" (change)="formatChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Subtitle output format for captions mode">
@for (f of formatOptions; track f.id) { @for (f of formatOptions; track f.id) {
<option [ngValue]="f.id">{{ f.text }}</option> <option [ngValue]="f.id">{{ f.text }}</option>
} }
@@ -294,7 +293,7 @@
</div> </div>
<div class="col-12 col-md-6 col-lg-3"> <div class="col-12 col-md-6 col-lg-3">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Language</span> <span class="input-group-text help-title" ngbPopover="Subtitle language (you can type any language code)." triggers="click" autoClose="outside" container="body">Language</span>
<input class="form-control" <input class="form-control"
type="text" type="text"
list="subtitleLanguageOptions" list="subtitleLanguageOptions"
@@ -302,8 +301,7 @@
[(ngModel)]="subtitleLanguage" [(ngModel)]="subtitleLanguage"
(change)="subtitleLanguageChanged()" (change)="subtitleLanguageChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading"
placeholder="e.g. en, es, zh-Hans" placeholder="e.g. en, es, zh-Hans">
ngbTooltip="Subtitle language (you can type any language code)">
<datalist id="subtitleLanguageOptions"> <datalist id="subtitleLanguageOptions">
@for (lang of subtitleLanguages; track lang.id) { @for (lang of subtitleLanguages; track lang.id) {
<option [value]="lang.id">{{ lang.text }}</option> <option [value]="lang.id">{{ lang.text }}</option>
@@ -313,13 +311,12 @@
</div> </div>
<div class="col-12 col-md-6 col-lg-3"> <div class="col-12 col-md-6 col-lg-3">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Subtitle Source</span> <span class="input-group-text help-title" ngbPopover="Choose manual, auto, or fallback preference for captions mode." triggers="click" autoClose="outside" container="body">Subtitle Source</span>
<select class="form-select" <select class="form-select"
name="subtitleMode" name="subtitleMode"
[(ngModel)]="subtitleMode" [(ngModel)]="subtitleMode"
(change)="subtitleModeChanged()" (change)="subtitleModeChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
@for (mode of subtitleModes; track mode.id) { @for (mode of subtitleModes; track mode.id) {
<option [ngValue]="mode.id">{{ mode.text }}</option> <option [ngValue]="mode.id">{{ mode.text }}</option>
} }
@@ -375,34 +372,29 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Download Folder</span> <span class="input-group-text help-title" ngbPopover="Type to filter existing folders, or enter a new folder name." triggers="click" autoClose="outside" container="body">Download Folder</span>
@if (customDirs$ | async; as customDirs) { <input type="text"
<ng-select [items]="customDirs" class="form-control"
placeholder="Default" placeholder="Default"
[addTag]="allowCustomDir.bind(this)" name="folder"
addTagText="Create directory"
bindLabel="folder"
[(ngModel)]="folder" [(ngModel)]="folder"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [ngbTypeahead]="searchFolder"
[virtualScroll]="true" [editable]="!!downloads.configuration['CREATE_CUSTOM_DIRS']"
[clearable]="true" (focus)="folderFocus$.next($any($event.target).value)"
[loading]="downloads.loading" (click)="folderClick$.next($any($event.target).value)"
[searchable]="true" #folderTypeahead="ngbTypeahead"
[closeOnSelect]="true" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Choose where to save downloads. Type to create a new folder." />
}
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Custom Name Prefix</span> <span class="input-group-text help-title" ngbPopover="Add a prefix to downloaded filenames." triggers="click" autoClose="outside" container="body">Custom Name Prefix</span>
<input type="text" <input type="text"
class="form-control" class="form-control"
placeholder="Default" placeholder="Default"
name="customNamePrefix" name="customNamePrefix"
[(ngModel)]="customNamePrefix" [(ngModel)]="customNamePrefix"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Add a prefix to downloaded filenames">
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
@@ -411,18 +403,16 @@
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkbox-split-chapters" <input class="form-check-input" type="checkbox" role="switch" id="checkbox-split-chapters"
name="splitByChapters" [(ngModel)]="splitByChapters" (change)="splitByChaptersChanged()" name="splitByChapters" [(ngModel)]="splitByChapters" (change)="splitByChaptersChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Split video into separate files by chapters">
<label class="form-check-label" for="checkbox-split-chapters">Split by chapters</label> <label class="form-check-label" for="checkbox-split-chapters">Split by chapters</label>
</div> </div>
</div> </div>
@if (splitByChapters) { @if (splitByChapters) {
<div class="col"> <div class="col">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Template</span> <span class="input-group-text help-title" ngbPopover="Output template for chapter files." triggers="click" autoClose="outside" container="body">Template</span>
<input type="text" class="form-control" name="chapterTemplate" [(ngModel)]="chapterTemplate" <input type="text" class="form-control" name="chapterTemplate" [(ngModel)]="chapterTemplate"
(change)="chapterTemplateChanged()" [disabled]="addInProgress || subscribeInProgress || downloads.loading" (change)="chapterTemplateChanged()" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Output template for chapter files">
</div> </div>
</div> </div>
} }
@@ -431,28 +421,26 @@
@if (downloadType === 'video' || downloadType === 'audio') { @if (downloadType === 'video' || downloadType === 'audio') {
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Clip start</span> <span class="input-group-text help-title" ngbPopover="Optional start time (seconds, M:SS, or H:MM:SS). Blank = from start or YouTube &t= in URL." triggers="click" autoClose="outside" container="body">Clip start</span>
<input type="text" <input type="text"
class="form-control" class="form-control"
name="clipStart" name="clipStart"
[(ngModel)]="clipStart" [(ngModel)]="clipStart"
(change)="clipStartChanged()" (change)="clipStartChanged()"
placeholder="e.g. 2:26" placeholder="e.g. 2:26"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Optional start time (seconds, M:SS, or H:MM:SS). Blank = from start or YouTube &t= in URL.">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Clip end</span> <span class="input-group-text help-title" ngbPopover="Optional end time. Blank = until end of media." triggers="click" autoClose="outside" container="body">Clip end</span>
<input type="text" <input type="text"
class="form-control" class="form-control"
name="clipEnd" name="clipEnd"
[(ngModel)]="clipEnd" [(ngModel)]="clipEnd"
(change)="clipEndChanged()" (change)="clipEndChanged()"
placeholder="e.g. 3:24" placeholder="e.g. 3:24"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Optional end time. Blank = until end of media.">
</div> </div>
</div> </div>
} }
@@ -463,13 +451,12 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Auto Start</span> <span class="input-group-text help-title" ngbPopover="Automatically start downloads when added." triggers="click" autoClose="outside" container="body">Auto Start</span>
<select class="form-select" <select class="form-select"
name="autoStart" name="autoStart"
[(ngModel)]="autoStart" [(ngModel)]="autoStart"
(change)="autoStartChanged()" (change)="autoStartChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Automatically start downloads when added">
<option [ngValue]="true">Yes</option> <option [ngValue]="true">Yes</option>
<option [ngValue]="false">No</option> <option [ngValue]="false">No</option>
</select> </select>
@@ -477,7 +464,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Items Limit</span> <span class="input-group-text help-title" ngbPopover="Maximum number of items to download from a playlist or channel (0 = no limit)." triggers="click" autoClose="outside" container="body">Items Limit</span>
<input type="number" <input type="number"
min="0" min="0"
class="form-control" class="form-control"
@@ -485,13 +472,12 @@
name="playlistItemLimit" name="playlistItemLimit"
(keydown)="isNumber($event)" (keydown)="isNumber($event)"
[(ngModel)]="playlistItemLimit" [(ngModel)]="playlistItemLimit"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Maximum number of items to download from a playlist or channel (0 = no limit)">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Subscription Check (min)</span> <span class="input-group-text help-title" ngbPopover="How often to poll subscriptions for new videos." triggers="click" autoClose="outside" container="body">Subscription Check (min)</span>
<input type="number" <input type="number"
min="1" min="1"
class="form-control" class="form-control"
@@ -499,29 +485,33 @@
(keydown)="isNumber($event)" (keydown)="isNumber($event)"
[(ngModel)]="checkIntervalMinutes" [(ngModel)]="checkIntervalMinutes"
(ngModelChange)="checkIntervalChanged()" (ngModelChange)="checkIntervalChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="How often to poll subscriptions for new videos">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Subscription Title Filter</span> <span class="input-group-text help-title" ngbPopover="In subscriptions, only titles matching this Python-style regex are queued. Empty = all. Case-sensitive; use (?i) in the pattern for case-insensitive." triggers="click" autoClose="outside" container="body">Subscription Title Filter</span>
<input type="text" <input type="text"
class="form-control" class="form-control"
name="titleRegex" name="titleRegex"
[(ngModel)]="titleRegex" [(ngModel)]="titleRegex"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading"
placeholder="Optional regex" placeholder="Optional regex">
ngbTooltip="In subscriptions, only titles matching this Python-style regex are queued. Empty = all. Case-sensitive; use (?i) in the pattern for case-insensitive.">
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkbox-skip-subscriber-only" <input class="form-check-input" type="checkbox" role="switch" id="checkbox-skip-subscriber-only"
name="skipSubscriberOnly" [(ngModel)]="skipSubscriberOnly" name="skipSubscriberOnly" [(ngModel)]="skipSubscriberOnly"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading" />
ngbTooltip="When enabled, subscription checks skip videos marked members-only by yt-dlp (channel Join). Ignored for one-off downloads." /> <label class="form-check-label" for="checkbox-skip-subscriber-only">
<label class="form-check-label" for="checkbox-skip-subscriber-only">Skip members-only subscription videos</label> <span class="help-title" tabindex="0" role="button"
ngbPopover="When enabled, subscription checks skip videos marked members-only by yt-dlp (channel Join). Ignored for one-off downloads."
triggers="click" autoClose="outside" container="body"
(click)="$event.preventDefault(); $event.stopPropagation()"
(keydown.enter)="$event.preventDefault(); $event.stopPropagation()"
(keydown.space)="$event.preventDefault(); $event.stopPropagation()">Skip members-only subscription videos</span>
</label>
</div> </div>
</div> </div>
</div> </div>
@@ -531,7 +521,7 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-12" [class.col-md-6]="allowYtdlOptionsOverrides()"> <div class="col-12" [class.col-md-6]="allowYtdlOptionsOverrides()">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Option Presets</span> <span class="input-group-text help-title" ngbPopover="Choose one or more yt-dlp option presets configured on the server (applied in order)." triggers="click" autoClose="outside" container="body">Option Presets</span>
<ng-select <ng-select
class="flex-grow-1" class="flex-grow-1"
name="ytdlOptionsPresets" name="ytdlOptionsPresets"
@@ -541,22 +531,20 @@
placeholder="Default" placeholder="Default"
[(ngModel)]="ytdlOptionsPresets" [(ngModel)]="ytdlOptionsPresets"
(ngModelChange)="ytdlOptionsPresetsChanged()" (ngModelChange)="ytdlOptionsPresetsChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading" />
ngbTooltip="Choose one or more yt-dlp option presets configured on the server (applied in order)" />
</div> </div>
</div> </div>
@if (allowYtdlOptionsOverrides()) { @if (allowYtdlOptionsOverrides()) {
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Custom yt-dlp Options</span> <span class="input-group-text help-title" ngbPopover="Optional per-download yt-dlp overrides as a JSON object." triggers="click" autoClose="outside" container="body">Custom yt-dlp Options</span>
<input type="text" <input type="text"
class="form-control" class="form-control"
placeholder='e.g. {"writesubtitles": true}' placeholder='e.g. {"writesubtitles": true}'
name="ytdlOptionsOverrides" name="ytdlOptionsOverrides"
[(ngModel)]="ytdlOptionsOverrides" [(ngModel)]="ytdlOptionsOverrides"
(change)="ytdlOptionsOverridesChanged()" (change)="ytdlOptionsOverridesChanged()"
[disabled]="addInProgress || subscribeInProgress || downloads.loading" [disabled]="addInProgress || subscribeInProgress || downloads.loading">
ngbTooltip="Optional per-download yt-dlp overrides as a JSON object">
</div> </div>
</div> </div>
} }
@@ -566,7 +554,7 @@
<div class="settings-section-label">Tools</div> <div class="settings-section-label">Tools</div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
<div class="action-group-label">Cookies</div> <div class="action-group-label help-title" ngbPopover="Upload a cookies.txt file from your browser to authenticate restricted or private downloads." triggers="click" autoClose="outside" container="body">Cookies</div>
<input type="file" id="cookie-upload" class="d-none" accept=".txt" <input type="file" id="cookie-upload" class="d-none" accept=".txt"
(change)="onCookieFileSelect($event)" (change)="onCookieFileSelect($event)"
[disabled]="cookieUploadInProgress || addInProgress"> [disabled]="cookieUploadInProgress || addInProgress">
@@ -574,8 +562,7 @@
<label class="btn mb-0" <label class="btn mb-0"
[class]="hasCookies ? 'btn cookie-active-btn mb-0' : 'btn cookie-btn mb-0'" [class]="hasCookies ? 'btn cookie-active-btn mb-0' : 'btn cookie-btn mb-0'"
[class.disabled]="cookieUploadInProgress || addInProgress" [class.disabled]="cookieUploadInProgress || addInProgress"
for="cookie-upload" for="cookie-upload">
ngbTooltip="Upload a cookies.txt file for authenticated downloads">
@if (cookieUploadInProgress) { @if (cookieUploadInProgress) {
<span class="spinner-border spinner-border-sm me-2" role="status"></span> <span class="spinner-border spinner-border-sm me-2" role="status"></span>
} @else { } @else {
@@ -939,8 +926,7 @@
</th> </th>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">URL</th> <th scope="col">URL</th>
<th scope="col" class="text-nowrap" <th scope="col" class="text-nowrap"><span class="help-title" ngbPopover="Subscriptions only — which new video titles to queue when this feed is checked. Does not affect manual downloads." triggers="click" autoClose="outside" container="body">Sub. title filter</span></th>
ngbTooltip="Subscriptions only — which new video titles to queue when this feed is checked. Does not affect manual downloads.">Sub. title filter</th>
<th scope="col" class="text-nowrap">Interval (min)</th> <th scope="col" class="text-nowrap">Interval (min)</th>
<th scope="col" class="text-nowrap">Last checked</th> <th scope="col" class="text-nowrap">Last checked</th>
<th scope="col">Status</th> <th scope="col">Status</th>
+8
View File
@@ -201,6 +201,14 @@ main
color: var(--bs-secondary-color) color: var(--bs-secondary-color)
margin-bottom: 0.4rem margin-bottom: 0.4rem
.help-title
text-decoration: underline dotted
text-underline-offset: 0.2em
cursor: help
&:focus
outline: none
.cookie-status .cookie-status
font-size: 0.8rem font-size: 0.8rem
margin-top: 0.35rem margin-top: 0.35rem
+27 -35
View File
@@ -1,12 +1,12 @@
import { AsyncPipe, DatePipe, KeyValuePipe, NgTemplateOutlet } from '@angular/common'; import { DatePipe, KeyValuePipe, NgTemplateOutlet } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, viewChild, inject, OnDestroy, OnInit } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, viewChild, inject, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject, Subscription, from, map, distinctUntilChanged, finalize, mergeMap, takeUntil, tap } from 'rxjs'; import { Observable, OperatorFunction, Subject, Subscription, from, map, merge, debounceTime, distinctUntilChanged, filter, finalize, mergeMap, takeUntil, tap } from 'rxjs';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule, NgbTypeahead } 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, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay, faShareNodes } from '@fortawesome/free-solid-svg-icons'; import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay, faShareNodes } 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';
@@ -40,7 +40,6 @@ import { SelectAllCheckboxComponent, ItemCheckboxComponent } from './components/
FormsModule, FormsModule,
NgTemplateOutlet, NgTemplateOutlet,
KeyValuePipe, KeyValuePipe,
AsyncPipe,
DatePipe, DatePipe,
FontAwesomeModule, FontAwesomeModule,
NgbModule, NgbModule,
@@ -105,8 +104,10 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
cookieUploadInProgress = false; cookieUploadInProgress = false;
themes: Theme[] = Themes; themes: Theme[] = Themes;
activeTheme: Theme | undefined; activeTheme: Theme | undefined;
customDirs$!: Observable<string[]>; readonly folderTypeahead = viewChild<NgbTypeahead>('folderTypeahead');
showBatchPanel = false; folderFocus$ = new Subject<string>();
folderClick$ = new Subject<string>();
showBatchPanel = false;
batchImportModalOpen = false; batchImportModalOpen = false;
batchImportText = ''; batchImportText = '';
batchImportStatus = ''; batchImportStatus = '';
@@ -309,7 +310,6 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
this.getConfiguration(); this.getConfiguration();
this.getYtdlOptionsUpdateTime(); this.getYtdlOptionsUpdateTime();
this.getYtdlOptionPresets(); this.getYtdlOptionPresets();
this.customDirs$ = this.getMatchingCustomDir();
this.setTheme(this.activeTheme!); this.setTheme(this.activeTheme!);
this.colorSchemeMediaQuery.addEventListener('change', this.onColorSchemeChanged); this.colorSchemeMediaQuery.addEventListener('change', this.onColorSchemeChanged);
@@ -337,9 +337,9 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
// workaround to allow fetching of Map values in the order they were inserted // workaround to allow fetching of Map values in the order they were inserted
// https://github.com/angular/angular/issues/31420 // https://github.com/angular/angular/issues/31420
asIsOrder() { asIsOrder() {
return 1; return 1;
} }
@@ -376,34 +376,26 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
return this.downloads.configuration['ALLOW_YTDL_OPTIONS_OVERRIDES'] === true; return this.downloads.configuration['ALLOW_YTDL_OPTIONS_OVERRIDES'] === true;
} }
allowCustomDir(tag: string) { searchFolder: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) => {
if (this.downloads.configuration['CREATE_CUSTOM_DIRS']) { const debouncedText$ = text$.pipe(debounceTime(150), distinctUntilChanged());
return tag; const clicksWithClosedPopup$ = this.folderClick$.pipe(
} filter(() => !this.folderTypeahead()?.isPopupOpen()),
return false; );
} return merge(debouncedText$, this.folderFocus$, clicksWithClosedPopup$).pipe(
map(term => {
const dirs = this.isAudioType()
? (this.downloads.customDirs?.['audio_download_dir'] ?? [])
: (this.downloads.customDirs?.['download_dir'] ?? []);
const t = (term ?? '').toLowerCase();
return (t === '' ? dirs : dirs.filter(d => d.toLowerCase().includes(t))).slice(0, 10);
}),
);
};
isAudioType() { isAudioType() {
return this.downloadType === 'audio'; return this.downloadType === 'audio';
} }
getMatchingCustomDir() : Observable<string[]> {
return this.downloads.customDirsChanged.asObservable().pipe(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map((output: any) => {
// 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"];
}
}),
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr))
);
}
getYtdlOptionsUpdateTime() { getYtdlOptionsUpdateTime() {
this.downloads.ytdlOptionsChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ this.downloads.ytdlOptionsChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1471,7 +1463,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
} }
fetchVersionInfo(): void { fetchVersionInfo(): void {
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const baseUrl = `${window.location.origin}${window.location.pathname.replace(/\/[^\/]*$/, '/')}`; const baseUrl = `${window.location.origin}${window.location.pathname.replace(/\/[^\/]*$/, '/')}`;
const versionUrl = `${baseUrl}version`; const versionUrl = `${baseUrl}version`;
this.http.get<{ 'yt-dlp': string, version: string }>(versionUrl) this.http.get<{ 'yt-dlp': string, version: string }>(versionUrl)