diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 3b595a8..5519620 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -131,33 +131,19 @@ } - -
- + +
- - -
- - -
diff --git a/ui/src/app/app.sass b/ui/src/app/app.sass index 97ba9ed..ecbac85 100644 --- a/ui/src/app/app.sass +++ b/ui/src/app/app.sass @@ -27,6 +27,34 @@ main max-width: 1024px margin: 2.5rem auto +.url-entry-wrap + display: flex + gap: .75rem + align-items: stretch + +.url-entry-textarea + min-height: 8.25rem + max-height: 12rem + resize: none + overflow-y: auto + +.url-entry-actions + display: flex + flex-direction: column + gap: .5rem + width: 11rem + + .btn + flex: 1 + white-space: nowrap + +@media (max-width: 767.98px) + .url-entry-wrap + flex-direction: column + + .url-entry-actions + width: 100% + .card border: 1px solid color-mix(in srgb, var(--bs-border-color) 75%, transparent) border-radius: var(--mt-radius-lg) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 04bf860..03f53ce 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1,7 +1,7 @@ import { AsyncPipe, DatePipe, KeyValuePipe, NgTemplateOutlet } from '@angular/common'; import { HttpClient } from '@angular/common/http'; 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, Subject, Subscription, from, map, distinctUntilChanged, finalize, mergeMap, takeUntil, tap, concatMap, toArray } from 'rxjs'; import { FormsModule } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @@ -1063,6 +1063,11 @@ export class App implements AfterViewInit, OnInit, OnDestroy { addDownload(overrides: Partial = {}) { const payload = this.buildAddPayload(overrides); + const urls = this.parseInputUrls(payload.url); + if (!urls.length) { + alert('Please enter at least one URL'); + return; + } // Validate chapter template if chapter splitting is enabled if (payload.splitByChapters && !payload.chapterTemplate.includes('%(section_number)')) { @@ -1077,16 +1082,27 @@ export class App implements AfterViewInit, OnInit, OnDestroy { this.addInProgress = true; this.cancelRequested = false; this.addRequestSub?.unsubscribe(); - this.addRequestSub = this.downloads.add(payload).subscribe((status: Status) => { - if (status.status === 'error' && !this.cancelRequested) { - alert(`Error adding URL: ${status.msg}`); - } else if (status.status !== 'error') { + this.addRequestSub = from(urls).pipe( + concatMap(url => this.downloads.add({ ...payload, url })), + toArray(), + ).subscribe((statuses: Status[]) => { + const failed = statuses.find(s => s.status === 'error'); + if (failed && !this.cancelRequested) { + alert(`Error adding URL: ${failed.msg}`); + } else if (!failed) { this.addUrl = ''; } this.resetAddState(); }); } + private parseInputUrls(raw: string): string[] { + return (raw || '') + .split(/[\s,\n\r]+/) + .map(url => url.trim()) + .filter(url => url.length > 0); + } + cancelAdding() { this.cancelRequested = true; this.downloads.cancelAdd().subscribe({