mirror of
https://github.com/neosubhamoy/pytubepp-helper.git
synced 2026-02-04 11:22:22 +05:30
(feat): added update notification preference toggle in settings
This commit is contained in:
924
package-lock.json
generated
924
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
"@radix-ui/react-toast": "^1.2.5",
|
"@radix-ui/react-toast": "^1.2.5",
|
||||||
"@radix-ui/react-tooltip": "^1.1.7",
|
"@radix-ui/react-tooltip": "^1.1.7",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
pub notify_updates: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -14,6 +15,7 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
port: 3030,
|
port: 3030,
|
||||||
theme: "system".to_string(),
|
theme: "system".to_string(),
|
||||||
|
notify_updates: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
pub notify_updates: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -14,6 +15,7 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
port: 3030,
|
port: 3030,
|
||||||
theme: "system".to_string(),
|
theme: "system".to_string(),
|
||||||
|
notify_updates: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/App.tsx
50
src/App.tsx
@@ -5,16 +5,20 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Config, WebSocketMessage } from "@/types";
|
import { Config, WebSocketMessage } from "@/types";
|
||||||
import { sendStreamInfo } from "@/lib/utils";
|
import { compareVersions, sendStreamInfo } from "@/lib/utils";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { check as checkAppUpdate } from "@tauri-apps/plugin-updater";
|
import { check as checkAppUpdate } from "@tauri-apps/plugin-updater";
|
||||||
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification";
|
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification";
|
||||||
|
import { downloadDir, join } from "@tauri-apps/api/path";
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http';
|
||||||
|
import * as fs from "@tauri-apps/plugin-fs"
|
||||||
|
|
||||||
function App({ children }: { children: React.ReactNode }) {
|
function App({ children }: { children: React.ReactNode }) {
|
||||||
const appWindow = getCurrentWebviewWindow()
|
const appWindow = getCurrentWebviewWindow()
|
||||||
const [appConfig, setAppConfig] = useState<Config | null>(null);
|
const [appConfig, setAppConfig] = useState<Config | null>(null);
|
||||||
const [isAppUpdateChecked, setIsAppUpdateChecked] = useState(false);
|
const [isAppUpdateChecked, setIsAppUpdateChecked] = useState(false);
|
||||||
|
const [isExtensionUpdateChecked, setIsExtensionUpdateChecked] = useState(false);
|
||||||
|
|
||||||
// Prevent right click context menu in production
|
// Prevent right click context menu in production
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
@@ -91,10 +95,50 @@ function App({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isAppUpdateChecked) {
|
const checkForExtensionUpdates = async () => {
|
||||||
|
let permissionGranted = await isPermissionGranted();
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission();
|
||||||
|
permissionGranted = permission === 'granted';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsExtensionUpdateChecked(true)
|
||||||
|
const downloadDirPath = await downloadDir()
|
||||||
|
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
|
||||||
|
const extensionManifestExists = await fs.exists(extensionManifestPath)
|
||||||
|
if (extensionManifestExists) {
|
||||||
|
const currentManifest = JSON.parse(await fs.readTextFile(extensionManifestPath))
|
||||||
|
const response = await fetch('https://github.com/neosubhamoy/pytubepp-extension/releases/latest/download/latest.json', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (compareVersions(data.version, currentManifest.version) === 1) {
|
||||||
|
console.log(`extension update available v${data.version}`);
|
||||||
|
if (permissionGranted) {
|
||||||
|
sendNotification({ title: `Extension Update Available (v${data.version})`, body: `A newer version of PytubePP Extension is available. Please update to the latest version to get the best experience!` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error('Failed to fetch latest extension version');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Currently installed extension\'s manifest not found')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAppUpdateChecked && appConfig?.notify_updates) {
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
}
|
}
|
||||||
}, [])
|
|
||||||
|
if (!isExtensionUpdateChecked && appConfig?.notify_updates) {
|
||||||
|
checkForExtensionUpdates();
|
||||||
|
}
|
||||||
|
}, [appConfig])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme={appConfig?.theme || "system"} storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme={appConfig?.theme || "system"} storageKey="vite-ui-theme">
|
||||||
|
|||||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
@@ -18,7 +18,7 @@ export async function isInstalled(program: string, arg: string): Promise<{ insta
|
|||||||
return { installed: false, output: output.stdout };
|
return { installed: false, output: output.stdout };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(program + ':', error);
|
||||||
return { installed: false, output: null };
|
return { installed: false, output: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,8 +234,6 @@ export default function HomePage() {
|
|||||||
const checkForExtensionUpdates = async () => {
|
const checkForExtensionUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
const downloadDirPath = await downloadDir()
|
const downloadDirPath = await downloadDir()
|
||||||
// const extensionDirPath = await join(downloadDirPath, "pytubepp-extension-chrome")
|
|
||||||
// const extensionDirExists = await fs.exists(extensionDirPath)
|
|
||||||
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
|
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
|
||||||
const extensionManifestExists = await fs.exists(extensionManifestPath)
|
const extensionManifestExists = await fs.exists(extensionManifestPath)
|
||||||
if (extensionManifestExists) {
|
if (extensionManifestExists) {
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|||||||
import { getPlatformInfo } from "@/lib/platform-utils";
|
import { getPlatformInfo } from "@/lib/platform-utils";
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useTheme } from "@/components/theme-provider"
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
const DEFAULT_PORT = 3030;
|
const DEFAULT_PORT = 3030;
|
||||||
const DEFAULT_THEME = "system";
|
const DEFAULT_THEME = "system";
|
||||||
|
const DEFAULT_NOTIFY_UPDATES = true;
|
||||||
const settingsFormSchema = z.object({
|
const settingsFormSchema = z.object({
|
||||||
port: z.number().min(3000, { message: "Port must be greater than 3000" }).max(3999, { message: "Port must be less than 3999" }),
|
port: z.number().min(3000, { message: "Port must be greater than 3000" }).max(3999, { message: "Port must be less than 3999" }),
|
||||||
theme: z.enum(["system", "dark", "light"]),
|
theme: z.enum(["system", "dark", "light"], { message: "Invalid theme" }),
|
||||||
|
notify_updates: z.boolean({ message: "Not a boolean value" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -37,13 +40,14 @@ export default function SettingsPage() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
port: DEFAULT_PORT,
|
port: DEFAULT_PORT,
|
||||||
theme: DEFAULT_THEME,
|
theme: DEFAULT_THEME,
|
||||||
|
notify_updates: DEFAULT_NOTIFY_UPDATES,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = settingsForm.watch((value) => {
|
const subscription = settingsForm.watch((value) => {
|
||||||
if (appConfig) {
|
if (appConfig) {
|
||||||
setIsFormDirty(value.port !== appConfig.port || value.theme !== appConfig.theme);
|
setIsFormDirty(value.port !== appConfig.port || value.theme !== appConfig.theme || value.notify_updates !== appConfig.notify_updates);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
@@ -54,7 +58,7 @@ export default function SettingsPage() {
|
|||||||
const config: Config = await invoke("get_config");
|
const config: Config = await invoke("get_config");
|
||||||
if (config) {
|
if (config) {
|
||||||
setAppConfig(config);
|
setAppConfig(config);
|
||||||
settingsForm.reset({ port: config.port, theme: config.theme });
|
settingsForm.reset({ port: config.port, theme: config.theme, notify_updates: config.notify_updates });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getConfig().catch(console.error);
|
getConfig().catch(console.error);
|
||||||
@@ -81,7 +85,8 @@ export default function SettingsPage() {
|
|||||||
const updatedConfig: Config = await invoke("update_config", {
|
const updatedConfig: Config = await invoke("update_config", {
|
||||||
newConfig: {
|
newConfig: {
|
||||||
port: Number(settingsForm.getValues().port),
|
port: Number(settingsForm.getValues().port),
|
||||||
theme: settingsForm.getValues().theme
|
theme: settingsForm.getValues().theme,
|
||||||
|
notify_updates: settingsForm.getValues().notify_updates,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setAppConfig(updatedConfig);
|
setAppConfig(updatedConfig);
|
||||||
@@ -97,7 +102,7 @@ export default function SettingsPage() {
|
|||||||
try {
|
try {
|
||||||
const config: Config = await invoke("reset_config");
|
const config: Config = await invoke("reset_config");
|
||||||
setAppConfig(config);
|
setAppConfig(config);
|
||||||
settingsForm.reset({ port: config.port, theme: config.theme });
|
settingsForm.reset({ port: config.port, theme: config.theme, notify_updates: config.notify_updates });
|
||||||
setIsFormDirty(false);
|
setIsFormDirty(false);
|
||||||
toast("Settings reset to default");
|
toast("Settings reset to default");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -106,7 +111,7 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsingDefaultConfig = appConfig?.port === DEFAULT_PORT && appConfig?.theme === DEFAULT_THEME;
|
const isUsingDefaultConfig = appConfig?.port === DEFAULT_PORT && appConfig?.theme === DEFAULT_THEME && appConfig?.notify_updates === DEFAULT_NOTIFY_UPDATES;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -152,7 +157,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
|
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
|
||||||
<div className="flex flex-col min-h-[55vh] overflow-y-scroll">
|
<div className="flex flex-col min-h-[55vh] max-h-[58vh] overflow-y-scroll">
|
||||||
<Form {...settingsForm}>
|
<Form {...settingsForm}>
|
||||||
<form onSubmit={settingsForm.handleSubmit(updateConfig)}>
|
<form onSubmit={settingsForm.handleSubmit(updateConfig)}>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -188,7 +193,7 @@ export default function SettingsPage() {
|
|||||||
control={settingsForm.control}
|
control={settingsForm.control}
|
||||||
name="theme"
|
name="theme"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="mb-2">
|
||||||
<FormLabel>Theme</FormLabel>
|
<FormLabel>Theme</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select {...field} onValueChange={(value) => field.onChange(value)}>
|
<Select {...field} onValueChange={(value) => field.onChange(value)}>
|
||||||
@@ -204,16 +209,39 @@ export default function SettingsPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Choose app interface theme
|
||||||
|
</FormDescription>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={settingsForm.control}
|
||||||
|
name="notify_updates"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm mb-4 mt-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Notify Updates</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Notify for app and component updates (Recommended)
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button className="hidden" ref={saveButtonRef} type="submit">Save</Button>
|
<Button className="hidden" ref={saveButtonRef} type="submit">Save</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center border-t border-muted-foreground/50 pt-2 relative">
|
<div className="flex justify-between items-center border-t border-muted-foreground/50 pt-2 relative">
|
||||||
<div className="tintbar absolute -top-[0.10rem] left-0 -translate-y-full w-full h-5 bg-gradient-to-b from-transparent to-background"></div>
|
<div className="tintbar absolute -top-[0.05rem] left-0 -translate-y-full w-full h-5 bg-gradient-to-b from-transparent to-background"></div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p>PytubePP Helper <span className="text-muted-foreground">|</span> <span className="text-sm text-muted-foreground">v{appVersion}-beta</span></p>
|
<p>PytubePP Helper <span className="text-muted-foreground">|</span> <span className="text-sm text-muted-foreground">v{appVersion}-beta</span></p>
|
||||||
<p className="text-xs text-muted-foreground">© {new Date().getFullYear()} - <a href="https://github.com/neosubhamoy/pytubepp-helper/blob/main/LICENSE" target="_blank">MIT License</a> - Made with ❤️ by <a href="https://neosubhamoy.com" target="_blank">Subhamoy</a></p>
|
<p className="text-xs text-muted-foreground">© {new Date().getFullYear()} - <a href="https://github.com/neosubhamoy/pytubepp-helper/blob/main/LICENSE" target="_blank">MIT License</a> - Made with ❤️ by <a href="https://neosubhamoy.com" target="_blank">Subhamoy</a></p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
theme: "system" | "dark" | "light";
|
theme: "system" | "dark" | "light";
|
||||||
|
notify_updates: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlatformInfo {
|
export interface PlatformInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user