Support multi-link input parsing and multiline URL composer UI
This commit is contained in:
parent
6f1723b44d
commit
bcb2560347
3 changed files with 54 additions and 24 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue