Support multi-link input parsing and multiline URL composer UI

This commit is contained in:
TonyBlu 2026-05-01 07:47:32 +08:00
parent 6f1723b44d
commit bcb2560347
3 changed files with 54 additions and 24 deletions

View file

@ -131,33 +131,19 @@
}
</ng-template>
<!-- Narrow viewports: full-width field, then Bootstrap btn-group (no faux input-group strip) -->
<div class="vstack gap-2 d-md-none">
<input type="text"
<div class="url-entry-wrap shadow-sm">
<textarea
autocomplete="off"
spellcheck="false"
class="form-control form-control-lg"
class="form-control form-control-lg url-entry-textarea"
placeholder="Enter one or multiple links (separate with spaces, commas, or newlines)"
[(ngModel)]="addUrl"
[ngModelOptions]="{standalone: true}"
[disabled]="addInProgress || subscribeInProgress || downloads.loading">
<div class="btn-group w-100" role="group" aria-label="Download or subscribe">
[disabled]="addInProgress || subscribeInProgress || downloads.loading"></textarea>
<div class="url-entry-actions" aria-label="Download or subscribe">
<ng-container [ngTemplateOutlet]="urlBarActions" />
</div>
</div>
<!-- md and up: standard input-group so Bootstrap handles fused borders -->
<div class="input-group input-group-lg shadow-sm d-none d-md-flex">
<input type="text"
autocomplete="off"
spellcheck="false"
class="form-control form-control-lg"
placeholder="Enter one or multiple links (separate with spaces, commas, or newlines)"
[(ngModel)]="addUrl"
[ngModelOptions]="{standalone: true}"
[disabled]="addInProgress || subscribeInProgress || downloads.loading">
<ng-container [ngTemplateOutlet]="urlBarActions" />
</div>
</div>
</div>

View file

@ -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)

View file

@ -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<AddDownloadPayload> = {}) {
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({