mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-19 13:12:57 +05:30
(feat): added speed rate limit option in settings, improved download error handling and improved remove from library dialog
This commit is contained in:
69
src/App.tsx
69
src/App.tsx
@@ -7,7 +7,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
import { RawVideoInfo } from "@/types/video";
|
||||
@@ -25,8 +25,11 @@ import { useNavigate } from "react-router-dom";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
||||
import useAppUpdater from "@/helpers/use-app-updater";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
export default function App({ children }: { children: React.ReactNode }) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
||||
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
||||
@@ -58,6 +61,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||
const USE_RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||
const RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||
const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||
const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||
const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||
@@ -65,6 +70,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||
const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||
|
||||
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const navigate = useNavigate();
|
||||
const { updateYtDlp } = useYtDlpUpdater();
|
||||
@@ -136,6 +148,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||
// set error states to default
|
||||
setIsErrored(false);
|
||||
setIsErrorExpected(false);
|
||||
setErroredDownloadId(null);
|
||||
|
||||
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||
console.error('FFmpeg or download paths not found');
|
||||
@@ -147,6 +164,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
||||
if (!videoMetadata) {
|
||||
console.error('Failed to fetch video metadata');
|
||||
toast({
|
||||
title: 'Download Failed',
|
||||
description: 'yt-dlp failed to fetch video metadata. Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,12 +255,20 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
args.push('--proxy', PROXY_URL);
|
||||
}
|
||||
|
||||
if (USE_RATE_LIMIT && RATE_LIMIT) {
|
||||
args.push('--limit-rate', `${RATE_LIMIT}`);
|
||||
}
|
||||
|
||||
console.log('Starting download with args:', args);
|
||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||
|
||||
command.on('close', async data => {
|
||||
if (data.code !== 0) {
|
||||
console.error(`Download failed with code ${data.code}`);
|
||||
if (!isErrorExpected) {
|
||||
setIsErrored(true);
|
||||
setErroredDownloadId(downloadId);
|
||||
}
|
||||
} else {
|
||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
||||
onSuccess: (data) => {
|
||||
@@ -430,6 +460,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const pauseDownload = async (downloadState: DownloadState) => {
|
||||
try {
|
||||
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||
console.log("Killing process with PID:", downloadState.process_id);
|
||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
||||
@@ -474,6 +505,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const cancelDownload = async (downloadState: DownloadState) => {
|
||||
try {
|
||||
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||
console.log("Killing process with PID:", downloadState.process_id);
|
||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||
}
|
||||
@@ -807,6 +839,41 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
||||
|
||||
// show a toast and pause the download when yt-dlp exits unexpectedly
|
||||
useEffect(() => {
|
||||
if (isErrored && !isErrorExpected) {
|
||||
toast({
|
||||
title: "Download Failed",
|
||||
description: "yt-dlp exited unexpectedly. Please try again later",
|
||||
variant: "destructive",
|
||||
});
|
||||
if (erroredDownloadId) {
|
||||
downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download status updated successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update download status:", error);
|
||||
}
|
||||
})
|
||||
setErroredDownloadId(null);
|
||||
}
|
||||
setIsErrored(false);
|
||||
setIsErrorExpected(false);
|
||||
}
|
||||
}, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]);
|
||||
|
||||
// auto reset isErrorExpected state after 3 seconds
|
||||
useEffect(() => {
|
||||
if (isErrorExpected) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setIsErrorExpected(false);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [isErrorExpected, setIsErrorExpected]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
||||
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAppContext } from "@/providers/appContextProvider";
|
||||
import { useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||
import { useDownloadActionStatesStore, useDownloaderPageStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
||||
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
@@ -33,6 +33,8 @@ export default function LibraryPage() {
|
||||
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
||||
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
||||
|
||||
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||
|
||||
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -106,6 +108,7 @@ export default function LibraryPage() {
|
||||
|
||||
const stopOngoingDownloads = async () => {
|
||||
if (ongoingDownloads.length > 0) {
|
||||
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||
try {
|
||||
await invoke('pause_ongoing_downloads').then(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
@@ -275,9 +278,9 @@ export default function LibraryPage() {
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Remove from library?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone! it will permanently remove this from downloads.
|
||||
Are you sure you want to remove this download from the library? You can also delete the downloaded file by cheking the box below. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SlidingButton } from "@/components/custom/slidingButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { formatSpeed } from "@/utils";
|
||||
|
||||
const websocketPortSchema = z.object({
|
||||
port: z.coerce.number({
|
||||
@@ -42,6 +43,17 @@ const proxyUrlSchema = z.object({
|
||||
url: z.string().min(1, { message: "Proxy URL is required" }).url({ message: "Invalid URL format" })
|
||||
});
|
||||
|
||||
const rateLimitSchema = z.object({
|
||||
rate_limit: z.coerce.number({
|
||||
required_error: "Rate Limit is required",
|
||||
invalid_type_error: "Rate Limit must be a valid number",
|
||||
}).min(1024, {
|
||||
message: "Rate Limit must be at least 1024 bytes/s (1 KB/s)"
|
||||
}).max(104857600, {
|
||||
message: "Rate Limit must be at most 104857600 bytes/s (100 MB/s)"
|
||||
}),
|
||||
});
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
const { setTheme } = useTheme();
|
||||
@@ -65,6 +77,8 @@ export default function SettingsPage() {
|
||||
const strictDownloadabilityCheck = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||
const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||
const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||
const useRateLimit = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||
const rateLimit = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||
const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||
@@ -169,6 +183,33 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const rateLimitForm = useForm<z.infer<typeof rateLimitSchema>>({
|
||||
resolver: zodResolver(rateLimitSchema),
|
||||
defaultValues: {
|
||||
rate_limit: rateLimit,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedRateLimit = rateLimitForm.watch("rate_limit");
|
||||
const { errors: rateLimitFormErrors } = rateLimitForm.formState;
|
||||
|
||||
function handleRateLimitSubmit(values: z.infer<typeof rateLimitSchema>) {
|
||||
try {
|
||||
saveSettingsKey('rate_limit', values.rate_limit);
|
||||
toast({
|
||||
title: "Rate Limit updated",
|
||||
description: `Rate Limit changed to ${values.rate_limit} bytes/s`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing rate limit:", error);
|
||||
toast({
|
||||
title: "Failed to change rate limit",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface Config {
|
||||
port: number;
|
||||
}
|
||||
@@ -577,36 +618,74 @@ export default function SettingsPage() {
|
||||
/>
|
||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Form {...proxyUrlForm}>
|
||||
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={proxyUrlForm.control}
|
||||
name="url"
|
||||
disabled={!useProxy}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter proxy URL"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedProxyUrl || watchedProxyUrl === proxyUrl || Object.keys(proxyUrlFormErrors).length > 0 || !useProxy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<Form {...proxyUrlForm}>
|
||||
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={proxyUrlForm.control}
|
||||
name="url"
|
||||
disabled={!useProxy}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter proxy URL"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedProxyUrl || watchedProxyUrl === proxyUrl || Object.keys(proxyUrlFormErrors).length > 0 || !useProxy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="rate-limit">
|
||||
<h3 className="font-semibold">Rate Limit</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Limit download speed to prevent network congestion. Rate limit is applied per-download basis (not in the whole app)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-rate-limit"
|
||||
checked={useRateLimit}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||
/>
|
||||
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||
</div>
|
||||
<Form {...rateLimitForm}>
|
||||
<form onSubmit={rateLimitForm.handleSubmit(handleRateLimitSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={rateLimitForm.control}
|
||||
name="rate_limit"
|
||||
disabled={!useRateLimit}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter rate limit in bytes/s"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedRateLimit || Number(watchedRateLimit) === rateLimit || Object.keys(rateLimitFormErrors).length > 0 || !useRateLimit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
@@ -49,13 +49,19 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
|
||||
selectedCombinableAudioFormat: '',
|
||||
selectedSubtitles: [],
|
||||
selectedPlaylistVideoIndex: '1',
|
||||
isErrored: false,
|
||||
isErrorExpected: false,
|
||||
erroredDownloadId: null,
|
||||
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })),
|
||||
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||
}));
|
||||
|
||||
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||
@@ -122,6 +128,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
max_parallel_downloads: 2,
|
||||
use_proxy: false,
|
||||
proxy_url: '',
|
||||
use_rate_limit: false,
|
||||
rate_limit: 1048576, // 1 MB/s
|
||||
video_format: 'auto',
|
||||
audio_format: 'auto',
|
||||
always_reencode_video: false,
|
||||
@@ -163,6 +171,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
max_parallel_downloads: 2,
|
||||
use_proxy: false,
|
||||
proxy_url: '',
|
||||
use_rate_limit: false,
|
||||
rate_limit: 1048576, // 1 MB/s
|
||||
video_format: 'auto',
|
||||
audio_format: 'auto',
|
||||
always_reencode_video: false,
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface Settings {
|
||||
strict_downloadablity_check: boolean;
|
||||
use_proxy: boolean;
|
||||
proxy_url: string;
|
||||
use_rate_limit: boolean;
|
||||
rate_limit: number;
|
||||
video_format: string;
|
||||
audio_format: string;
|
||||
always_reencode_video: boolean;
|
||||
|
||||
@@ -38,6 +38,9 @@ export interface DownloaderPageStatesStore {
|
||||
selectedCombinableAudioFormat: string;
|
||||
selectedSubtitles: string[];
|
||||
selectedPlaylistVideoIndex: string;
|
||||
isErrored: boolean;
|
||||
isErrorExpected: boolean;
|
||||
erroredDownloadId: string | null;
|
||||
setActiveDownloadModeTab: (tab: string) => void;
|
||||
setIsStartingDownload: (isStarting: boolean) => void;
|
||||
setSelectedDownloadFormat: (format: string) => void;
|
||||
@@ -45,6 +48,9 @@ export interface DownloaderPageStatesStore {
|
||||
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||
setIsErrored: (isErrored: boolean) => void;
|
||||
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||
setErroredDownloadId: (downloadId: string | null) => void;
|
||||
}
|
||||
|
||||
export interface LibraryPageStatesStore {
|
||||
|
||||
Reference in New Issue
Block a user