mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 21:45:04 +00:00
review fixes
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { DownloadsService } from './downloads.service';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
export type BatchUrlFilter = 'pending' | 'completed' | 'failed' | 'all';
|
||||
|
||||
/**
|
||||
* Encapsulates collecting download URLs by status and exporting/copying them.
|
||||
* Extracted from the main app component to keep it focused on view concerns.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BatchUrlsService {
|
||||
private downloads = inject(DownloadsService);
|
||||
private toasts = inject(ToastService);
|
||||
|
||||
collect(filter: BatchUrlFilter): string[] {
|
||||
const queueUrls = () => Array.from(this.downloads.queue.values()).map((dl) => dl.url);
|
||||
const doneUrls = (status?: string) =>
|
||||
Array.from(this.downloads.done.values())
|
||||
.filter((dl) => status === undefined || dl.status === status)
|
||||
.map((dl) => dl.url);
|
||||
switch (filter) {
|
||||
case 'pending':
|
||||
return queueUrls();
|
||||
case 'completed':
|
||||
return doneUrls('finished');
|
||||
case 'failed':
|
||||
return doneUrls('error');
|
||||
default:
|
||||
return [...queueUrls(), ...doneUrls()];
|
||||
}
|
||||
}
|
||||
|
||||
export(filter: BatchUrlFilter): void {
|
||||
const urls = this.collect(filter);
|
||||
if (!urls.length) {
|
||||
this.toasts.info('No URLs found for the selected filter.');
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([urls.join('\n')], { type: 'text/plain' });
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = 'metube_urls.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
copy(filter: BatchUrlFilter): void {
|
||||
const urls = this.collect(filter);
|
||||
if (!urls.length) {
|
||||
this.toasts.info('No URLs found for the selected filter.');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(urls.join('\n'))
|
||||
.then(() => this.toasts.success('URLs copied to clipboard.'))
|
||||
.catch(() => this.toasts.error('Failed to copy URLs.'));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { DownloadsService } from './downloads.service';
|
||||
export { MeTubeSocket } from './metube-socket.service';
|
||||
export { MeTubeSocket } from './metube-socket.service';
|
||||
export { ToastService } from './toast.service';
|
||||
export { BatchUrlsService } from './batch-urls.service';
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export type ToastLevel = 'info' | 'success' | 'error';
|
||||
|
||||
export interface ToastAction {
|
||||
label: string;
|
||||
value: boolean;
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
id: number;
|
||||
level: ToastLevel;
|
||||
message: string;
|
||||
actions?: ToastAction[];
|
||||
/** Resolver for confirm() toasts; resolved when the user picks an action or dismisses. */
|
||||
_resolve?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight non-blocking notification service. Replaces the blocking
|
||||
* window.alert()/confirm() dialogs that previously littered the app component.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ToastService {
|
||||
private counter = 0;
|
||||
readonly toasts = signal<Toast[]>([]);
|
||||
|
||||
info(message: string): void {
|
||||
this.show('info', message, 4000);
|
||||
}
|
||||
|
||||
success(message: string): void {
|
||||
this.show('success', message, 4000);
|
||||
}
|
||||
|
||||
error(message: string): void {
|
||||
this.show('error', message, 8000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a confirmation toast with confirm/cancel actions. Resolves true when
|
||||
* confirmed, false when cancelled or auto-dismissed.
|
||||
*/
|
||||
confirm(message: string, confirmLabel = 'OK', cancelLabel = 'Cancel'): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const id = ++this.counter;
|
||||
this.toasts.update((list) => [
|
||||
...list,
|
||||
{
|
||||
id,
|
||||
level: 'info',
|
||||
message,
|
||||
actions: [
|
||||
{ label: cancelLabel, value: false },
|
||||
{ label: confirmLabel, value: true, primary: true },
|
||||
],
|
||||
_resolve: resolve,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
respond(id: number, value: boolean): void {
|
||||
const toast = this.toasts().find((t) => t.id === id);
|
||||
toast?._resolve?.(value);
|
||||
this.remove(id);
|
||||
}
|
||||
|
||||
dismiss(id: number): void {
|
||||
const toast = this.toasts().find((t) => t.id === id);
|
||||
// A confirm toast dismissed without an explicit choice resolves to false.
|
||||
toast?._resolve?.(false);
|
||||
this.remove(id);
|
||||
}
|
||||
|
||||
private remove(id: number): void {
|
||||
this.toasts.update((list) => list.filter((t) => t.id !== id));
|
||||
}
|
||||
|
||||
private show(level: ToastLevel, message: string, autoDismissMs: number): void {
|
||||
const id = ++this.counter;
|
||||
this.toasts.update((list) => [...list, { id, level, message }]);
|
||||
setTimeout(() => this.remove(id), autoDismissMs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user