mirror of
https://github.com/neosubhamoy/pytubepp-helper.git
synced 2026-02-04 11:22:22 +05:30
(feat): added notifications page and app updater
This commit is contained in:
66
package-lock.json
generated
66
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
2
src-tauri/Cargo.lock
generated
@@ -3334,7 +3334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pytubepp-helper"
|
||||
version = "0.7.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"fix-path-env",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "pytubepp-helper"
|
||||
version = "0.7.0"
|
||||
version = "0.6.0"
|
||||
description = "PytubePP Helper"
|
||||
authors = ["neosubhamoy"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
12
src/App.tsx
12
src/App.tsx
@@ -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);
|
||||
|
||||
35
src/components/ui/badge.tsx
Normal file
35
src/components/ui/badge.tsx
Normal 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 }
|
||||
75
src/components/ui/card.tsx
Normal file
75
src/components/ui/card.tsx
Normal 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 }
|
||||
37
src/components/ui/notification-badge.tsx
Normal file
37
src/components/ui/notification-badge.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
27
src/components/ui/progress.tsx
Normal file
27
src/components/ui/progress.tsx
Normal 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 }
|
||||
@@ -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>
|
||||
|
||||
@@ -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
151
src/pages/notifications.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user