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({