mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-20 01:59:33 +05:30
feat: added notification options
This commit is contained in:
46
src/App.tsx
46
src/App.tsx
@@ -29,6 +29,7 @@ import { toast } from "sonner";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { DownloadConfiguration } from "@/types/settings";
|
||||
import { ulid } from "ulid";
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification';
|
||||
|
||||
export default function App({ children }: { children: React.ReactNode }) {
|
||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||
@@ -92,6 +93,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const LOG_VERBOSE = useSettingsPageStatesStore(state => state.settings.log_verbose);
|
||||
const LOG_WARNING = useSettingsPageStatesStore(state => state.settings.log_warning);
|
||||
const LOG_PROGRESS = useSettingsPageStatesStore(state => state.settings.log_progress);
|
||||
const ENABLE_NOTIFICATIONS = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const DOWNLOAD_COMPLETION_NOTIFICATION = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
||||
|
||||
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||
@@ -125,6 +128,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const isProcessingQueueRef = useRef(false);
|
||||
const lastProcessedDownloadIdRef = useRef<string | null>(null);
|
||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||
const hasRunAppUpdateCheckRef = useRef(false);
|
||||
const isRegisteredToMacOsRef = useRef(false);
|
||||
|
||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration): Promise<RawVideoInfo | null> => {
|
||||
@@ -541,8 +545,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const downloadedFileExt = downloadFilePath.split('.').pop();
|
||||
|
||||
// Update completion status after a short delay to ensure database states are propagated correctly
|
||||
console.log(`Download completed with ID: ${downloadId}, updating filepath and status after 2s delay...`);
|
||||
setTimeout(() => {
|
||||
console.log(`Download completed with ID: ${downloadId}, updating filepath and status after 1.5s delay...`);
|
||||
setTimeout(async () => {
|
||||
LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}`);
|
||||
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath as string, ext: downloadedFileExt as string }, {
|
||||
onSuccess: (data) => {
|
||||
@@ -563,7 +567,18 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
console.error("Failed to update download status:", error);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
toast.success("Download Completed", {
|
||||
description: `The download for "${videoMetadata.title}" has completed successfully.`,
|
||||
});
|
||||
|
||||
if (ENABLE_NOTIFICATIONS && DOWNLOAD_COMPLETION_NOTIFICATION) {
|
||||
sendNotification({
|
||||
title: "Download Completed",
|
||||
body: `The download for "${videoMetadata.title}" has completed successfully.`,
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -869,13 +884,6 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for App updates
|
||||
useEffect(() => {
|
||||
checkForAppUpdate().catch((error) => {
|
||||
console.error("Error checking for app update:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Prevent app from closing
|
||||
useEffect(() => {
|
||||
const handleCloseRequested = (event: any) => {
|
||||
@@ -1025,6 +1033,24 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
fetchYtDlpVersion();
|
||||
}, [ytDlpVersion, setYtDlpVersion]);
|
||||
|
||||
// Check for app update
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||
console.log("Skipping app update check, waiting for configs to load...");
|
||||
return;
|
||||
}
|
||||
// Skip if we've already run the update check once
|
||||
if (hasRunAppUpdateCheckRef.current) {
|
||||
console.log("App update check already performed in this session, skipping");
|
||||
return;
|
||||
}
|
||||
hasRunAppUpdateCheckRef.current = true;
|
||||
checkForAppUpdate().catch((error) => {
|
||||
console.error("Error checking for app update:", error);
|
||||
});
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
// Check for yt-dlp auto-update
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { config } from "@/config";
|
||||
import { check as checkAppUpdate, Update } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch as relaunchApp } from "@tauri-apps/plugin-process";
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification';
|
||||
|
||||
export default function useAppUpdater() {
|
||||
const setIsCheckingAppUpdate = useSettingsPageStatesStore(state => state.setIsCheckingAppUpdate);
|
||||
const setAppUpdate = useSettingsPageStatesStore(state => state.setAppUpdate);
|
||||
const setIsUpdating = useSettingsPageStatesStore(state => state.setIsUpdatingApp);
|
||||
const setDownloadProgress = useSettingsPageStatesStore(state => state.setAppUpdateDownloadProgress);
|
||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||
const LOG = useLogger();
|
||||
|
||||
const checkForAppUpdate = async () => {
|
||||
setIsCheckingAppUpdate(true);
|
||||
@@ -15,6 +21,13 @@ export default function useAppUpdater() {
|
||||
if (update) {
|
||||
setAppUpdate(update);
|
||||
console.log(`app update available v${update.version}`);
|
||||
LOG.info('NEODLP', `App update available v${update.version}`);
|
||||
if (enableNotifications && updateNotification) {
|
||||
sendNotification({
|
||||
title: `Update Available (v${update.version})`,
|
||||
body: `A newer version of ${config.appName} is available. Please update to the latest version for the best experience`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -25,6 +38,7 @@ export default function useAppUpdater() {
|
||||
|
||||
const downloadAndInstallAppUpdate = async (update: Update) => {
|
||||
setIsUpdating(true);
|
||||
LOG.info('NEODLP', `Downloading and installing app update v${update.version}`);
|
||||
let downloaded = 0;
|
||||
let contentLength: number | undefined = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
@@ -52,4 +66,4 @@ export default function useAppUpdater() {
|
||||
checkForAppUpdate,
|
||||
downloadAndInstallAppUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowDownToLine, ArrowRight, BrushCleaning, Bug, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { ArrowDownToLine, ArrowRight, BellRing, BrushCleaning, Bug, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@/providers/themeProvider";
|
||||
@@ -30,6 +30,7 @@ import { formatSpeed, generateID } from "@/utils";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
||||
|
||||
const websocketPortSchema = z.object({
|
||||
port: z.coerce.number<number>({
|
||||
@@ -127,6 +128,9 @@ export default function SettingsPage() {
|
||||
const logVerbose = useSettingsPageStatesStore(state => state.settings.log_verbose);
|
||||
const logWarning = useSettingsPageStatesStore(state => state.settings.log_warning);
|
||||
const logProgress = useSettingsPageStatesStore(state => state.settings.log_progress);
|
||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||
const downloadCompletionNotification = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
||||
|
||||
const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
||||
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
||||
@@ -524,6 +528,11 @@ export default function SettingsPage() {
|
||||
value="sponsorblock"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><ShieldMinus className="size-4" /> Sponsorblock</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="notifications"
|
||||
value="notifications"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><BellRing className="size-4" /> Notifications</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="commands"
|
||||
value="commands"
|
||||
@@ -536,7 +545,7 @@ export default function SettingsPage() {
|
||||
><Bug className="size-4" /> Debug</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="max-parallel-downloads">
|
||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
||||
@@ -592,7 +601,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="app-theme">
|
||||
<h3 className="font-semibold">Theme</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||
@@ -615,7 +624,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="download-dir">
|
||||
<h3 className="font-semibold">Download Folder</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
||||
@@ -705,7 +714,7 @@ export default function SettingsPage() {
|
||||
</Form>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="video-format">
|
||||
<h3 className="font-semibold">Video Format</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose in which format the final video file will be saved</p>
|
||||
@@ -773,7 +782,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="embed-video-metadata">
|
||||
<h3 className="font-semibold">Embed Metadata</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Wheather to embed metadata in video/audio files (info, chapters)</p>
|
||||
@@ -807,7 +816,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="proxy">
|
||||
<h3 className="font-semibold">Proxy</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)</p>
|
||||
@@ -923,7 +932,7 @@ export default function SettingsPage() {
|
||||
<Label className="text-xs text-muted-foreground">(Forced: {forceInternetProtocol === "ipv4" ? 'IPv4' : 'IPv6'}, Status: {useForceInternetProtocol && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="cookies" value="cookies" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="cookies" value="cookies" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="cookies">
|
||||
<h3 className="font-semibold">Cookies</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use cookies to access exclusive/private (login-protected) contents from sites (use wisely, over-use can even block/ban your account)</p>
|
||||
@@ -1012,7 +1021,7 @@ export default function SettingsPage() {
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {importCookiesFrom === "browser" ? 'Yes' : cookiesFile ? 'Yes' : 'No'}, From: {importCookiesFrom === "browser" ? 'Browser' : 'Text'}, Status: {useCookies && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="sponsorblock" value="sponsorblock" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="sponsorblock" value="sponsorblock" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="sponsorblock">
|
||||
<h3 className="font-semibold">Sponsor Block</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||
@@ -1136,7 +1145,56 @@ export default function SettingsPage() {
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {sponsorblockMode === "remove" && sponsorblockRemove === "custom" && sponsorblockRemoveCategories.length <= 0 ? 'No' : sponsorblockMode === "mark" && sponsorblockMark === "custom" && sponsorblockMarkCategories.length <= 0 ? 'No' : 'Yes'}, Mode: {sponsorblockMode === "remove" ? 'Remove' : 'Mark'}, Status: {useSponsorblock && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="commands" value="commands" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="notifications" value="notifications" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="notifications">
|
||||
<h3 className="font-semibold">Desktop Notifications</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Enable desktop notifications for app events (updates, download completions, etc.)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="enable-notifications"
|
||||
checked={enableNotifications}
|
||||
onCheckedChange={async (checked) => {
|
||||
if (checked) {
|
||||
const granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
const permission = await requestPermission();
|
||||
if (permission !== 'granted') {
|
||||
toast.error("Notification Permission Denied", {
|
||||
description: "You have denied the notification permission. Please enable it from your system settings to receive notifications.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
saveSettingsKey('enable_notifications', checked)
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="enable-notifications">Enable Notifications</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-5">
|
||||
<Label className="text-xs mb-1">Notification Categories</Label>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="update-notification"
|
||||
checked={updateNotification}
|
||||
onCheckedChange={(checked) => saveSettingsKey('update_notification', checked)}
|
||||
disabled={!enableNotifications}
|
||||
/>
|
||||
<Label htmlFor="update-notification">App Updates</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="download-completion-notification"
|
||||
checked={downloadCompletionNotification}
|
||||
onCheckedChange={(checked) => saveSettingsKey('download_completion_notification', checked)}
|
||||
disabled={!enableNotifications}
|
||||
/>
|
||||
<Label htmlFor="download-completion-notification">Download Completion</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="commands" value="commands" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="custom-commands">
|
||||
<h3 className="font-semibold">Custom Commands</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3"> Run custom yt-dlp commands for your downloads</p>
|
||||
@@ -1238,7 +1296,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="debug" value="debug" className="flex flex-col gap-4 min-h-[385px]">
|
||||
<TabsContent key="debug" value="debug" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="debug-mode">
|
||||
<h3 className="font-semibold">Debug Mode</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Enable debug mode for troubleshooting issues (get debug logs, download ids, and more)</p>
|
||||
|
||||
@@ -184,6 +184,9 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
log_verbose: true,
|
||||
log_warning: true,
|
||||
log_progress: false,
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
@@ -249,6 +252,9 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
log_verbose: true,
|
||||
log_warning: true,
|
||||
log_progress: false,
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface Settings {
|
||||
log_verbose: boolean;
|
||||
log_warning: boolean;
|
||||
log_progress: boolean;
|
||||
enable_notifications: boolean;
|
||||
update_notification: boolean;
|
||||
download_completion_notification: boolean;
|
||||
// extension settings
|
||||
websocket_port: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user