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>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Narrow viewports: full-width field, then Bootstrap btn-group (no faux input-group strip) -->
|
<div class="url-entry-wrap shadow-sm">
|
||||||
<div class="vstack gap-2 d-md-none">
|
<textarea
|
||||||
<input type="text"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
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)"
|
placeholder="Enter one or multiple links (separate with spaces, commas, or newlines)"
|
||||||
[(ngModel)]="addUrl"
|
[(ngModel)]="addUrl"
|
||||||
[ngModelOptions]="{standalone: true}"
|
[ngModelOptions]="{standalone: true}"
|
||||||
[disabled]="addInProgress || subscribeInProgress || downloads.loading">
|
[disabled]="addInProgress || subscribeInProgress || downloads.loading"></textarea>
|
||||||
<div class="btn-group w-100" role="group" aria-label="Download or subscribe">
|
<div class="url-entry-actions" aria-label="Download or subscribe">
|
||||||
<ng-container [ngTemplateOutlet]="urlBarActions" />
|
<ng-container [ngTemplateOutlet]="urlBarActions" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,34 @@ main
|
||||||
max-width: 1024px
|
max-width: 1024px
|
||||||
margin: 2.5rem auto
|
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
|
.card
|
||||||
border: 1px solid color-mix(in srgb, var(--bs-border-color) 75%, transparent)
|
border: 1px solid color-mix(in srgb, var(--bs-border-color) 75%, transparent)
|
||||||
border-radius: var(--mt-radius-lg)
|
border-radius: var(--mt-radius-lg)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { AsyncPipe, DatePipe, KeyValuePipe, NgTemplateOutlet } from '@angular/common';
|
import { AsyncPipe, 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, Subject, Subscription, from, map, distinctUntilChanged, finalize, mergeMap, takeUntil, tap, concatMap, toArray } 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';
|
||||||
|
|
@ -1063,6 +1063,11 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||||
|
|
||||||
addDownload(overrides: Partial<AddDownloadPayload> = {}) {
|
addDownload(overrides: Partial<AddDownloadPayload> = {}) {
|
||||||
const payload = this.buildAddPayload(overrides);
|
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
|
// Validate chapter template if chapter splitting is enabled
|
||||||
if (payload.splitByChapters && !payload.chapterTemplate.includes('%(section_number)')) {
|
if (payload.splitByChapters && !payload.chapterTemplate.includes('%(section_number)')) {
|
||||||
|
|
@ -1077,16 +1082,27 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||||
this.addInProgress = true;
|
this.addInProgress = true;
|
||||||
this.cancelRequested = false;
|
this.cancelRequested = false;
|
||||||
this.addRequestSub?.unsubscribe();
|
this.addRequestSub?.unsubscribe();
|
||||||
this.addRequestSub = this.downloads.add(payload).subscribe((status: Status) => {
|
this.addRequestSub = from(urls).pipe(
|
||||||
if (status.status === 'error' && !this.cancelRequested) {
|
concatMap(url => this.downloads.add({ ...payload, url })),
|
||||||
alert(`Error adding URL: ${status.msg}`);
|
toArray(),
|
||||||
} else if (status.status !== 'error') {
|
).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.addUrl = '';
|
||||||
}
|
}
|
||||||
this.resetAddState();
|
this.resetAddState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseInputUrls(raw: string): string[] {
|
||||||
|
return (raw || '')
|
||||||
|
.split(/[\s,\n\r]+/)
|
||||||
|
.map(url => url.trim())
|
||||||
|
.filter(url => url.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
cancelAdding() {
|
cancelAdding() {
|
||||||
this.cancelRequested = true;
|
this.cancelRequested = true;
|
||||||
this.downloads.cancelAdd().subscribe({
|
this.downloads.cancelAdd().subscribe({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue