1
1
mirror of https://github.com/neosubhamoy/neodlp.git synced 2026-03-22 19:25:49 +05:30

(chore): initial MVP release v0.1.0

This commit is contained in:
2025-04-28 23:49:42 +05:30
Verified
commit c73022b1a2
200 changed files with 24562 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { cn } from "@/lib/utils"
import { VideoFormat } from "@/types/video"
import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils"
import { File, Music, Video } from "lucide-react"
interface FormatSelectionGroupItemProps extends
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
format: VideoFormat
}
const FormatSelectionGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-3", className)}
{...props}
ref={ref}
/>
)
})
FormatSelectionGroup.displayName = "FormatSelectionGroup"
const FormatSelectionGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
FormatSelectionGroupItemProps
>(({ className, format, ...props }, ref) => {
const determineFileTypeIcon = (format: VideoFormat) => {
const fileFormat = determineFileType(/*format.video_ext, format.audio_ext,*/ format.vcodec, format.acodec)
switch (fileFormat) {
case 'video+audio':
return (
<span className="absolute flex items-center right-2 bottom-2">
<Video className="w-3 h-3 mr-2" />
<Music className="w-3 h-3" />
</span>
)
case 'video':
return (
<Video className="w-3 h-3 absolute right-2 bottom-2" />
)
case 'audio':
return (
<Music className="w-3 h-3 absolute right-2 bottom-2" />
)
default:
return (
<File className="w-3 h-3 absolute right-2 bottom-2" />
)
}
}
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"relative w-full rounded-lg border-2 border-border bg-card px-3 py-2 shadow-sm transition-all",
"data-[state=checked]:border-primary data-[state=checked]:border-2 data-[state=checked]:bg-muted/70",
"hover:bg-muted/70",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<div className="flex flex-col items-start text-start gap-1">
<h5 className="text-sm">{format.format}</h5>
<p className="text-muted-foreground text-xs">{format.filesize_approx ? formatFileSize(format.filesize_approx) : 'unknown'} {format.tbr ? formatBitrate(format.tbr) : 'unknown'}</p>
<p className="text-muted-foreground text-xs">{format.ext ? format.ext.toUpperCase() : 'unknown'} {
((format.vcodec && format.vcodec !== 'none') || (format.acodec && format.acodec !== 'none')) && (
`(${format.vcodec && format.vcodec !== 'none' ? formatCodec(format.vcodec) : ''}${format.vcodec && format.vcodec !== 'none' && format.acodec && format.acodec !== 'none' ? ' ' : ''}${format.acodec && format.acodec !== 'none' ? formatCodec(format.acodec) : ''})`
)}</p>
{determineFileTypeIcon(format)}
</div>
</RadioGroupPrimitive.Item>
)
})
FormatSelectionGroupItem.displayName = "FormatSelectionGroupItem"
export { FormatSelectionGroup, FormatSelectionGroupItem }

View File

@@ -0,0 +1,36 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
interface ProgressProps
extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
indeterminate?: boolean;
}
const IndeterminateProgress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
ProgressProps
>(({ className, value, indeterminate = false, ...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={cn(
"h-full w-full flex-1 bg-primary transition-all",
indeterminate && "animate-indeterminate-progress origin-left"
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
IndeterminateProgress.displayName = ProgressPrimitive.Root.displayName;
export { IndeterminateProgress };

View File

@@ -0,0 +1,82 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { cn } from "@/lib/utils"
import { RawVideoInfo } from "@/types/video"
import { formatDurationString} from "@/utils"
import { Clock } from "lucide-react"
import { AspectRatio } from "@/components/ui/aspect-ratio"
import { ProxyImage } from "@/components/custom/proxyImage"
import clsx from "clsx"
interface PlaylistSelectionGroupItemProps extends
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
video: RawVideoInfo;
}
const PlaylistSelectionGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-3", className)}
{...props}
ref={ref}
/>
)
})
PlaylistSelectionGroup.displayName = "PlaylistSelectionGroup"
const PlaylistSelectionGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
PlaylistSelectionGroupItemProps
>(({ className, video, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"relative w-full rounded-lg border-2 border-border bg-card p-2 shadow-sm transition-all",
"data-[state=checked]:border-primary data-[state=checked]:border-2 data-[state=checked]:bg-muted/70",
"hover:bg-muted/70",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<div className="flex gap-2 w-full relative">
<div className="w-[7rem] xl:w-[10rem]">
<AspectRatio
ratio={16 / 9}
className={clsx(
"w-full rounded overflow-hidden border border-border",
video.aspect_ratio && video.aspect_ratio === 0.56 && "relative"
)}
>
<ProxyImage
src={video.thumbnail}
alt="thumbnail"
className={clsx(
video.aspect_ratio && video.aspect_ratio === 0.56 &&
"absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
)}
/>
</AspectRatio>
</div>
<div className="flex w-[10rem] lg:w-[12rem] xl:w-[15rem] flex-col items-start text-start">
<h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1" title={video.title}>{video.title}</h3>
<p className="text-xs text-muted-foreground mb-2">{video.channel || video.uploader || 'unknown'}</p>
<div className="flex items-center">
<span className="text-xs text-muted-foreground flex items-center pr-3">
<Clock className="w-4 h-4 mr-2"/>
{video.duration_string ? formatDurationString(video.duration_string) : 'unknown'}
</span>
</div>
</div>
</div>
</RadioGroupPrimitive.Item>
)
})
PlaylistSelectionGroupItem.displayName = "PlaylistSelectionGroupItem"
export { PlaylistSelectionGroup, PlaylistSelectionGroupItem }

View File

@@ -0,0 +1,177 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
import { Checkbox } from "@/components/ui/checkbox";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { ProxyImage } from "@/components/custom/proxyImage";
import { Clock } from "lucide-react";
import clsx from "clsx";
import { formatDurationString } from "@/utils";
import { RawVideoInfo } from "@/types/video";
// Create a context to share toggle group props
const PlaylistToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { toggleType?: "single" | "multiple" }
>({
size: "default",
variant: "default",
toggleType: "multiple",
});
// Helper type for the PlaylistToggleGroup
type PlaylistToggleGroupProps =
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "single", value?: string, onValueChange?: (value: string) => void })
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "multiple", value?: string[], onValueChange?: (value: string[]) => void });
// Main PlaylistToggleGroup component with proper type handling
export const PlaylistToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
PlaylistToggleGroupProps
>(({ className, variant, size, children, type = "multiple", ...props }, ref) => {
// Pass props based on the type
if (type === "single") {
return (
<ToggleGroupPrimitive.Root
ref={ref}
type="single"
className={cn("flex flex-col gap-2", className)}
{...(props as any)}
>
<PlaylistToggleGroupContext.Provider value={{ variant, size, toggleType: "single" }}>
{children}
</PlaylistToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
return (
<ToggleGroupPrimitive.Root
ref={ref}
type="multiple"
className={cn("flex flex-col gap-2", className)}
{...(props as any)}
>
<PlaylistToggleGroupContext.Provider value={{ variant, size, toggleType: "multiple" }}>
{children}
</PlaylistToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
});
PlaylistToggleGroup.displayName = "PlaylistToggleGroup";
// Rest of your component remains the same
// PlaylistToggleGroupItem component with checkbox and item layout
export const PlaylistToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants> & {
video: RawVideoInfo;
}
>(({ className, children, variant, size, video, value, ...props }, ref) => {
const [isHovered, setIsHovered] = React.useState(false);
const [checked, setChecked] = React.useState(false);
// Instead of a ref + useEffect approach
const [itemElement, setItemElement] = React.useState<HTMLButtonElement | null>(null);
// Handle checkbox click separately by simulating a click on the parent item
const handleCheckboxClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
// Manually trigger the item's click to toggle selection
if (itemElement) {
// This simulates a click on the toggle item itself
itemElement.click();
}
};
// Use an effect that triggers when itemElement changes
React.useEffect(() => {
if (itemElement) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-state') {
setChecked(itemElement.getAttribute('data-state') === 'on');
}
});
});
setChecked(itemElement.getAttribute('data-state') === 'on');
observer.observe(itemElement, { attributes: true });
return () => observer.disconnect();
}
}, [itemElement]);
return (
<ToggleGroupPrimitive.Item
ref={(el) => {
// Handle both our ref and the forwarded ref
if (typeof ref === 'function') {
ref(el);
} else if (ref) {
ref.current = el;
}
setItemElement(el);
}}
className={cn(
"flex w-full p-2 rounded-md transition-colors border-2 border-border",
"hover:bg-muted/50 data-[state=on]:bg-muted/70",
"data-[state=on]:border-primary",
className
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
value={value}
{...props}
>
<div className="flex gap-2 w-full relative">
<div className="absolute top-2 left-2 z-10">
<Checkbox
checked={checked}
onClick={handleCheckboxClick}
className={cn(
"transition-opacity",
isHovered || checked ? "opacity-100" : "opacity-0"
)}
/>
</div>
<div className="w-[7rem] xl:w-[10rem]">
<AspectRatio
ratio={16 / 9}
className={clsx(
"w-full rounded overflow-hidden border border-border",
video.aspect_ratio && video.aspect_ratio === 0.56 && "relative"
)}
>
<ProxyImage
src={video.thumbnail}
alt="thumbnail"
className={clsx(
video.aspect_ratio && video.aspect_ratio === 0.56 &&
"absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
)}
/>
</AspectRatio>
</div>
<div className="flex w-[10rem] lg:w-[12rem] xl:w-[15rem] flex-col items-start text-start">
<h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1">{video.title}</h3>
<p className="text-xs text-muted-foreground mb-2">{video.channel || video.uploader || 'unknown'}</p>
<div className="flex items-center">
<span className="text-xs text-muted-foreground flex items-center pr-3">
<Clock className="w-4 h-4 mr-2"/>
{video.duration_string ? formatDurationString(video.duration_string) : 'unknown'}
</span>
</div>
</div>
</div>
</ToggleGroupPrimitive.Item>
);
});
PlaylistToggleGroupItem.displayName = "PlaylistToggleGroupItem";

View File

@@ -0,0 +1,116 @@
import { useState, useEffect } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { invoke } from "@tauri-apps/api/core";
import { Image } from "lucide-react";
interface ProxyImageProps {
src: string;
alt: string;
className?: string;
}
export function ProxyImage({ src, alt, className }: ProxyImageProps) {
const [proxiedSrc, setProxiedSrc] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [needsProxy, setNeedsProxy] = useState(false);
const [proxiedImageFailed, setProxiedImageFailed] = useState(false);
// Try direct loading first, fall back to proxy if needed
useEffect(() => {
setIsLoading(true);
setError(null);
setNeedsProxy(false);
setProxiedSrc(null);
setProxiedImageFailed(false);
if (!src) {
setError("No image source provided");
setIsLoading(false);
return;
}
return () => {
// Cleanup
};
}, [src]);
// Load via proxy if direct loading failed
useEffect(() => {
let isMounted = true;
async function loadImageViaProxy() {
if (!needsProxy || !src) return;
try {
setIsLoading(true);
const localPath = await invoke<string>("fetch_image", { url: src });
if (isMounted) {
setProxiedSrc(localPath);
setIsLoading(false);
}
} catch (err) {
// console.error("Error fetching image:", err);
if (isMounted) {
setError(err instanceof Error ? err.message : "Failed to load image");
setIsLoading(false);
}
}
}
if (needsProxy) {
loadImageViaProxy();
}
return () => {
isMounted = false;
};
}, [src, needsProxy]);
// Direct loading render
if (!needsProxy && !error) {
return (
<>
<img
src={src}
alt={alt}
className={className}
style={{ display: isLoading ? 'none' : 'block' }}
onLoad={() => setIsLoading(false)}
onError={() => {
// console.log("Direct image loading failed, falling back to proxy");
setNeedsProxy(true);
}}
/>
{isLoading && <Skeleton className={`w-full h-full ${className}`} />}
</>
);
}
// Proxy loading or error states
if (isLoading) {
return <Skeleton className={`w-full h-full ${className}`} />;
}
if (error || !proxiedSrc || proxiedImageFailed) {
return (
<div className={`flex items-center justify-center h-full bg-muted ${className}`}>
<Image className="w-7" />
</div>
);
}
// Successful proxy image
return (
<img
src={proxiedSrc}
alt={alt}
className={className}
onError={() => {
// console.log("Proxied image failed to render properly");
setProxiedImageFailed(true);
}}
/>
);
}