1
1
mirror of https://github.com/neosubhamoy/pytubepp-helper.git synced 2026-02-04 03:12:22 +05:30

(feat): added notifications page and app updater

This commit is contained in:
2025-02-13 09:30:35 +05:30
Verified
parent f6686f7e37
commit d6c5ff4be8
13 changed files with 436 additions and 11 deletions

66
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",
@@ -1177,6 +1178,71 @@
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz",
"integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",

View File

@@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",

2
src-tauri/Cargo.lock generated
View File

@@ -3334,7 +3334,7 @@ dependencies = [
[[package]]
name = "pytubepp-helper"
version = "0.7.0"
version = "0.6.0"
dependencies = [
"directories",
"fix-path-env",

View File

@@ -1,6 +1,6 @@
[package]
name = "pytubepp-helper"
version = "0.7.0"
version = "0.6.0"
description = "PytubePP Helper"
authors = ["neosubhamoy"]
edition = "2021"

View File

@@ -20,7 +20,7 @@
},
"productName": "pytubepp-helper",
"mainBinaryName": "pytubepp-helper",
"version": "0.7.0",
"version": "0.6.0",
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {
"updater": {

View File

@@ -15,7 +15,7 @@ function App({ children }: { children: React.ReactNode }) {
const appWindow = getCurrentWebviewWindow()
const [isAppUpdateChecked, setIsAppUpdateChecked] = useState(false);
// Prevent context menu in production
// Prevent right click context menu in production
if (!import.meta.env.DEV) {
document.oncontextmenu = (event) => {
event.preventDefault()
@@ -66,14 +66,14 @@ function App({ children }: { children: React.ReactNode }) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
setIsAppUpdateChecked(true);
try {
setIsAppUpdateChecked(true);
const update = await checkAppUpdate();
if (update) {
console.log(`found update ${update.version} from ${update.date} with notes ${update.body}`);
if (permissionGranted) {
sendNotification({ title: 'Update Available', body: 'A new version of pytubepp-helper is available. Please update to the latest version.' });
}
console.log(`app update available v${update.version}`);
if (permissionGranted) {
sendNotification({ title: `Update Available (v${update.version})`, body: `A newer version of PytubePP Helper is available. Please update to the latest version to get the best experience!` });
}
}
} catch (error) {
console.error(error);

View File

@@ -0,0 +1,35 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,75 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,37 @@
import { Badge, BadgeProps } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
export interface NotificationBadgeProps extends BadgeProps {
label?: string | number;
show?: boolean;
}
export const NotificationBadge = ({
label,
className,
show,
children,
...props
}: NotificationBadgeProps) => {
const showBadge =
typeof label !== 'undefined' && (typeof show === 'undefined' || show);
return (
<div className='inline-flex relative'>
{children}
{showBadge && (
<Badge
className={cn(
'absolute top-0 right-0 rounded-full',
typeof label !== 'undefined' && ('' + label).length === 0
? 'translate-x-1 -translate-y-1 px-1.5 py-1.5'
: 'translate-x-1.5 -translate-y-1.5 px-2',
className
)}
{...props}
>
{'' + label}
</Badge>
)}
</div>
);
};

View File

@@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -5,6 +5,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import App from "@/App";
import HomePage from "@/pages/home";
import SettingsPage from "@/pages/settings";
import NotificationsPage from "@/pages/notifications";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
@@ -13,6 +14,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/notifications" element={<NotificationsPage />} />
</Routes>
</BrowserRouter>
</App>

View File

@@ -6,10 +6,12 @@ import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { InstalledPrograms } from "@/types";
import { compareVersions, extractVersion, isInstalled, registerMacFiles } from "@/lib/utils";
import { CircleCheck, TriangleAlert, CircleAlert, Settings, RefreshCcw, Loader2, PackagePlus } from "lucide-react";
import { CircleCheck, TriangleAlert, CircleAlert, Settings, RefreshCcw, Loader2, PackagePlus, Bell } from "lucide-react";
import { getPlatformInfo } from "@/lib/platform-utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast";
import { NotificationBadge } from "@/components/ui/notification-badge";
import { check as checkAppUpdate } from "@tauri-apps/plugin-updater";
export default function HomePage() {
const { toast } = useToast();
@@ -20,6 +22,7 @@ export default function HomePage() {
const [macOsVersion, setMacOsVersion] = useState<string | null>(null)
const [distroId, setDistroId] = useState<string | null>(null)
const [distroPkgMngr, setDistroPkgMngr] = useState<string | null>(null)
const [isAppUpdateAvailable, setIsAppUpdateAvailable] = useState(false);
const [installedPrograms, setInstalledPrograms] = useState<InstalledPrograms>({
winget: {
installed: false,
@@ -199,6 +202,18 @@ export default function HomePage() {
init();
}, []);
useEffect(() => {
const checkForUpdates = async () => {
try {
const update = await checkAppUpdate();
setIsAppUpdateAvailable(update ? true : false);
} catch (error) {
console.error(error);
}
};
checkForUpdates();
}, []);
return (
<div className="container">
<div className={clsx("topbar flex justify-between items-center mt-5", !isWindows && "mx-3")}>
@@ -206,7 +221,23 @@ export default function HomePage() {
<div className="flex items-center">
<Tooltip>
<TooltipTrigger>
<Button variant="outline" size="icon" asChild>
<NotificationBadge
label='1'
className='bg-green-700 text-white hover:bg-green-700 hover:cursor-default'
show={isAppUpdateAvailable}
>
<Button variant="outline" size="icon" asChild>
<Link to="/notifications">
<Bell className="w-5 h-5"/>
</Link>
</Button>
</NotificationBadge>
</TooltipTrigger>
<TooltipContent>notifications</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button className="ml-3" variant="outline" size="icon" asChild>
<Link to="/settings">
<Settings className="w-5 h-5"/>
</Link>

151
src/pages/notifications.tsx Normal file
View File

@@ -0,0 +1,151 @@
import clsx from "clsx";
import { useState, useEffect } from "react";
import { getPlatformInfo } from "@/lib/platform-utils";
import { PlatformInfo } from "@/types";
import { ArrowLeft, Download, Loader2, RefreshCcw } from "lucide-react";
import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { check as checkAppUpdate, Update } from "@tauri-apps/plugin-updater";
import { relaunch as relaunchApp } from "@tauri-apps/plugin-process";
import { Progress } from "@/components/ui/progress";
export default function NotificationsPage() {
const [platformInfo, setPlatformInfo] = useState<PlatformInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [appUpdate, setAppUpdate] = useState<Update | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
async function checkForUpdates() {
setIsLoading(true);
try {
const update = await checkAppUpdate();
if (update) {
setAppUpdate(update);
console.log(`app update available v${update.version}`);
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
async function downloadAndInstallUpdate(update: Update) {
setIsUpdating(true);
let downloaded = 0;
let contentLength: number | undefined = 0;
await update.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
contentLength = event.data.contentLength;
console.log(`started downloading ${event.data.contentLength} bytes`);
break;
case 'Progress':
downloaded += event.data.chunkLength;
setDownloadProgress(downloaded / (contentLength || 0));
console.log(`downloaded ${downloaded} from ${contentLength}`);
break;
case 'Finished':
console.log('download finished');
setIsUpdating(false);
break;
}
});
await relaunchApp();
}
useEffect(() => {
getPlatformInfo().then(setPlatformInfo).catch(console.error);
checkForUpdates();
}, [])
return (
<div className="container">
<div className={clsx("topbar flex justify-between items-center mt-5", !platformInfo?.isWindows && "mx-3")}>
<div className="flex items-center">
<Link to="/" className={clsx(isUpdating && "pointer-events-none opacity-50")}>
<ArrowLeft className="w-5 h-5 mr-3"/>
</Link>
<h1 className="text-xl font-bold">Notifications</h1>
</div>
<div className="flex items-center">
<Tooltip>
<TooltipTrigger>
<Button className="ml-3" size="icon" disabled={isLoading || isUpdating} onClick={checkForUpdates}>
<RefreshCcw className="w-5 h-5"/>
</Button>
</TooltipTrigger>
<TooltipContent><p>refresh</p></TooltipContent>
</Tooltip>
</div>
</div>
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
{
isLoading ? (
<div className="mt-5 mx-3">
<div className="flex flex-col min-h-[55vh]">
<div className="flex items-center justify-center py-[4.3rem]">
<div className="flex flex-col items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin"/>
<p className="ml-3 mt-2 text-muted-foreground">ckecking...</p>
</div>
</div>
</div>
</div>
) : appUpdate ? (
<div className="mt-5">
<div className="flex flex-col min-h-[55vh]">
<Card className="">
<CardHeader>
<CardTitle>PytubePP Helper v{appUpdate.version} - Update Available</CardTitle>
<CardDescription>A newer version of PytubePP Helper is available. Please update to the latest version to get the best experience!</CardDescription>
{
isUpdating && (
<Progress value={downloadProgress * 100}/>
)
}
</CardHeader>
<CardFooter className="flex justify-between">
<div>
{
isUpdating && (
<div className="flex items-center">
<Loader2 className="w-4 h-4 mr-2 animate-spin"/>
<p className="text-sm text-muted-foreground">Downloading...</p>
</div>
)
}
</div>
<div>
<Button variant="link" size="sm" asChild>
<a href="https://github.com/neosubhamoy/pytubepp-helper/releases/latest" target="_blank"> Changelog</a>
</Button>
<Button className="ml-3" size="sm" disabled={isUpdating} onClick={() => downloadAndInstallUpdate(appUpdate)}>
<Download className="w-4 h-4 mr-2"/>
Update
</Button>
</div>
</CardFooter>
</Card>
</div>
</div>
) : (
<div className="mt-5 mx-3">
<div className="flex flex-col min-h-[55vh]">
<div className="flex items-center justify-center py-[4.3rem]">
<div className="flex flex-col items-center justify-center">
<p className="font-semibold ml-3 mt-2">No Notifications</p>
<p className="text-sm ml-3 mt-1 text-muted-foreground">You are all caught up! for now 😉</p>
</div>
</div>
</div>
</div>
)
}
</div>
</div>
)
}