feat: added square crop thumbnail config

This commit is contained in:
2025-12-16 20:31:38 +05:30
parent 1f06b73238
commit c1c2384c78
16 changed files with 866 additions and 840 deletions

686
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,9 +39,9 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.8",
"@tanstack/react-query-devtools": "^5.90.2",
"@tauri-apps/api": "^2.9.0",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.1",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
@@ -57,36 +57,36 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"es-toolkit": "^1.41.0",
"es-toolkit": "^1.43.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"lucide-react": "^0.561.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.0",
"react": "^19.2.3",
"react-day-picker": "^9.12.0",
"react-dom": "^19.2.3",
"react-hook-form": "^7.68.0",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.9.5",
"recharts": "^3.4.1",
"react-router-dom": "^7.10.1",
"recharts": "^3.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"ulid": "^3.0.1",
"ulid": "^3.0.2",
"vaul": "^1.1.2",
"zod": "^4.1.12",
"zustand": "^5.0.8"
"zod": "^4.2.0",
"zustand": "^5.0.9"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/vite": "^4.1.17",
"@tauri-apps/cli": "^2.9.4",
"@types/node": "^24.10.1",
"@types/react": "^19.2.3",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/cli": "^2.9.6",
"@types/node": "^25.0.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.1.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "^7.2.2"
"vite": "^7.3.0"
}
}

688
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -151,8 +151,83 @@ pub fn get_migrations() -> Vec<Migration> {
},
Migration {
version: 3,
description: "add_performance_indexes",
description: "add_more_columns_and_indexes_to_downloads",
sql: "
-- Create temporary table with all new columns
CREATE TABLE downloads_temp (
id INTEGER PRIMARY KEY NOT NULL,
download_id TEXT UNIQUE NOT NULL,
download_status TEXT NOT NULL,
video_id TEXT NOT NULL,
format_id TEXT NOT NULL,
subtitle_id TEXT,
queue_index INTEGER,
playlist_id TEXT,
playlist_index INTEGER,
resolution TEXT,
ext TEXT,
abr REAL,
vbr REAL,
acodec TEXT,
vcodec TEXT,
dynamic_range TEXT,
process_id INTEGER,
status TEXT,
progress REAL,
total INTEGER,
downloaded INTEGER,
speed REAL,
eta INTEGER,
filepath TEXT,
filetype TEXT,
filesize INTEGER,
output_format TEXT,
embed_metadata INTEGER NOT NULL DEFAULT 0,
embed_thumbnail INTEGER NOT NULL DEFAULT 0,
square_crop_thumbnail INTEGER NOT NULL DEFAULT 0,
sponsorblock_remove TEXT,
sponsorblock_mark TEXT,
use_aria2 INTEGER NOT NULL DEFAULT 0,
custom_command TEXT,
queue_config TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (video_id) REFERENCES video_info (video_id),
FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id)
);
-- Copy all data from original table to temporary table with default values for new columns
INSERT INTO downloads_temp SELECT
id, download_id, download_status, video_id, format_id, subtitle_id,
queue_index, playlist_id, playlist_index, resolution, ext, abr, vbr,
acodec, vcodec, dynamic_range, process_id, status, progress, total,
downloaded, speed, eta, filepath, filetype, filesize,
output_format,
embed_metadata,
embed_thumbnail,
0, -- square_crop_thumbnail
sponsorblock_remove, sponsorblock_mark, use_aria2,
custom_command, queue_config, created_at, updated_at
FROM downloads;
-- Remove existing triggers
DROP TRIGGER IF EXISTS update_downloads_updated_at;
-- Drop the original table
DROP TABLE downloads;
-- Rename temporary table to original name
ALTER TABLE downloads_temp RENAME TO downloads;
-- Create trigger for updating updated_at timestamp
CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at
AFTER UPDATE ON downloads
FOR EACH ROW
BEGIN
UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
-- Add indexes to improve query performance
CREATE INDEX IF NOT EXISTS idx_downloads_video_id ON downloads(video_id);
CREATE INDEX IF NOT EXISTS idx_downloads_playlist_id ON downloads(playlist_id);
CREATE INDEX IF NOT EXISTS idx_downloads_status_updated ON downloads(download_status, updated_at DESC);

View File

@@ -14,6 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
interface DownloadConfigDialogProps {
selectedFormatFileType: "video+audio" | "video" | "audio" | "unknown";
@@ -221,6 +222,15 @@ function DownloadConfigDialog({ selectedFormatFileType }: DownloadConfigDialogPr
disabled={useCustomCommands}
/>
<Label htmlFor="embed-thumbnail">Embed Thumbnail</Label>
<div className="flex items-center gap-3 ml-4">
<Checkbox
id="square-crop-thumbnail"
checked={downloadConfiguration.square_crop_thumbnail !== null ? downloadConfiguration.square_crop_thumbnail : false}
onCheckedChange={(checked) => setDownloadConfigurationKey('square_crop_thumbnail', checked)}
disabled={useCustomCommands || !(downloadConfiguration.embed_thumbnail !== null ? downloadConfiguration.embed_thumbnail : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoThumbnail : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioThumbnail : false)}
/>
<Label htmlFor="square-crop-thumbnail">Square Crop</Label>
</div>
</div>
</div>
</TabsContent>

View File

@@ -19,16 +19,13 @@ interface SelectivePlaylistDownloadProps {
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
allFilteredFormats: VideoFormat[];
subtitleLanguages: { code: string; lang: string }[];
selectedFormat: VideoFormat | undefined;
}
interface CombinedPlaylistDownloadProps {
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
selectedFormat: VideoFormat | undefined;
}
interface PlaylistDownloaderProps {
@@ -37,9 +34,7 @@ interface PlaylistDownloaderProps {
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
allFilteredFormats: VideoFormat[];
subtitleLanguages: { code: string; lang: string }[];
selectedFormat: VideoFormat | undefined;
}
function PlaylistPreviewSelection({ videoMetadata }: PlaylistPreviewSelectionProps) {
@@ -104,12 +99,13 @@ function PlaylistPreviewSelection({ videoMetadata }: PlaylistPreviewSelectionPro
);
}
function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, allFilteredFormats, subtitleLanguages, selectedFormat }: SelectivePlaylistDownloadProps) {
function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: SelectivePlaylistDownloadProps) {
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
@@ -120,7 +116,7 @@ function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyF
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
// disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
@@ -141,10 +137,11 @@ function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyF
value={selectedDownloadFormat}
onValueChange={(value) => {
setSelectedDownloadFormat(value);
const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
setSelectedSubtitles([]);
}
// const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
// setSelectedSubtitles([]);
// }
resetDownloadConfiguration();
}}
>
<p className="text-xs">Suggested</p>
@@ -217,13 +214,14 @@ function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyF
);
}
function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLanguages, selectedFormat }: CombinedPlaylistDownloadProps) {
function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLanguages }: CombinedPlaylistDownloadProps) {
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
@@ -234,7 +232,6 @@ function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitle
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
@@ -256,6 +253,7 @@ function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitle
value={selectedCombinableAudioFormat}
onValueChange={(value) => {
setSelectedCombinableAudioFormat(value);
resetDownloadConfiguration();
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
@@ -277,6 +275,7 @@ function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitle
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
resetDownloadConfiguration();
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
@@ -308,11 +307,12 @@ function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitle
);
}
export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, allFilteredFormats, subtitleLanguages, selectedFormat }: PlaylistDownloaderProps) {
export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: PlaylistDownloaderProps) {
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
const playlistPanelSizes = useDownloaderPageStatesStore((state) => state.playlistPanelSizes);
const setPlaylistPanelSizes = useDownloaderPageStatesStore((state) => state.setPlaylistPanelSizes);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex">
@@ -334,7 +334,10 @@ export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyF
<Tabs
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
onValueChange={(tab) => {
setActiveDownloadModeTab(tab);
resetDownloadConfiguration();
}}
>
<div className="flex items-center justify-between">
<h3 className="text-sm flex items-center gap-2">
@@ -353,9 +356,7 @@ export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyF
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
qualityPresetFormats={qualityPresetFormats}
allFilteredFormats={allFilteredFormats}
subtitleLanguages={subtitleLanguages}
selectedFormat={selectedFormat}
/>
</TabsContent>
<TabsContent value="combine">
@@ -363,7 +364,6 @@ export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyF
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
subtitleLanguages={subtitleLanguages}
selectedFormat={selectedFormat}
/>
</TabsContent>
</Tabs>

View File

@@ -97,7 +97,7 @@ function SelectiveVideoDownload({ videoMetadata, audioOnlyFormats, videoOnlyForm
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
@@ -133,10 +133,7 @@ function SelectiveVideoDownload({ videoMetadata, audioOnlyFormats, videoOnlyForm
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
// setSelectedSubtitles([]);
// }
setDownloadConfigurationKey('output_format', null);
setDownloadConfigurationKey('embed_metadata', null);
setDownloadConfigurationKey('embed_thumbnail', null);
setDownloadConfigurationKey('sponsorblock', null);
resetDownloadConfiguration();
}}
>
<p className="text-xs">Suggested</p>
@@ -216,7 +213,7 @@ function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLan
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
@@ -248,10 +245,7 @@ function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLan
value={selectedCombinableAudioFormat}
onValueChange={(value) => {
setSelectedCombinableAudioFormat(value);
setDownloadConfigurationKey('output_format', null);
setDownloadConfigurationKey('embed_metadata', null);
setDownloadConfigurationKey('embed_thumbnail', null);
setDownloadConfigurationKey('sponsorblock', null);
resetDownloadConfiguration();
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
@@ -273,10 +267,7 @@ function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLan
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
setDownloadConfigurationKey('output_format', null);
setDownloadConfigurationKey('embed_metadata', null);
setDownloadConfigurationKey('embed_thumbnail', null);
setDownloadConfigurationKey('sponsorblock', null);
resetDownloadConfiguration();
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
@@ -311,7 +302,7 @@ function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLan
export function VideoDownloader({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: VideoDownloaderProps) {
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
const videoPanelSizes = useDownloaderPageStatesStore((state) => state.videoPanelSizes);
const setVideoPanelSizes = useDownloaderPageStatesStore((state) => state.setVideoPanelSizes);
@@ -336,11 +327,8 @@ export function VideoDownloader({ videoMetadata, audioOnlyFormats, videoOnlyForm
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => {
setActiveDownloadModeTab(tab)
setDownloadConfigurationKey('output_format', null);
setDownloadConfigurationKey('embed_metadata', null);
setDownloadConfigurationKey('embed_thumbnail', null);
setDownloadConfigurationKey('sponsorblock', null);
setActiveDownloadModeTab(tab);
resetDownloadConfiguration();
}}
>
<div className="flex items-center justify-between">

View File

@@ -1295,9 +1295,9 @@ function AppInfoSettings() {
{ key: 'directories', name: 'Directories', desc: 'A Rust library for platform-specific standard locations', url: 'https://crates.io/crates/directories', license: 'MIT, Apache-2.0', licenseUrl: 'https://codeberg.org/dirs/directories-rs/src/branch/main/LICENSE-APACHE' },
];
function DependencyItem(dep: { key: string, name: string; desc: string; url: string; license: string; licenseUrl: string }) {
function DependencyItem(dep: { name: string; desc: string; url: string; license: string; licenseUrl: string }) {
return (
<div key={dep.key} className="p-4 border border-border rounded-md flex items-center justify-between gap-4">
<div className="p-4 border border-border rounded-md flex items-center justify-between gap-4">
<div className="flex flex-col">
<h4 className="font-semibold flex items-center gap-2">
<a href={dep.url} target="_blank" className="hover:underline">
@@ -1379,7 +1379,7 @@ function AppInfoSettings() {
<span className="flex items-center gap-4 flex-wrap">
<Button className="px-4" variant="outline" size="sm" asChild>
<a href={'mailto:' + config.appSupportEmail + '?subject=[BUG]%20Title%20Here&body=Describe%20The%20Bug%20Here.%20Follow%20this%20issue%20template%3A%20https%3A%2F%2Fgithub.com%2Fneosubhamoy%2Fneodlp%2Fissues%2Fnew%3Ftemplate%3Dbug_report.md'} target="_blank" >
<Mail className="size-4" /> Write an Email
<Mail className="size-4" /> Write Us an Email
</a>
</Button>
<Button className="px-4" size="sm" asChild>
@@ -1415,16 +1415,16 @@ function AppInfoSettings() {
</DialogHeader>
<div className="flex flex-col gap-4 max-h-[45vh] overflow-y-auto">
<h4 className="text-sm font-semibold">External Binaries</h4>
{binDepsList.map((dep) => (
<DependencyItem {...dep} />
{binDepsList.map(({key, ...dep}) => (
<DependencyItem key={key} {...dep} />
))}
<h4 className="text-sm font-semibold">Languages, Frameworks & Tooling</h4>
{langDepsList.map((dep) => (
<DependencyItem {...dep} />
{langDepsList.map(({key, ...dep}) => (
<DependencyItem key={key} {...dep} />
))}
<h4 className="text-sm font-semibold">Notable Libraries</h4>
{libDepsList.map((dep) => (
<DependencyItem {...dep} />
{libDepsList.map(({key, ...dep}) => (
<DependencyItem key={key} {...dep} />
))}
</div>
</DialogContent>

View File

@@ -1,28 +1,31 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,42 +1,44 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
}
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
ref={ref}
data-slot="radio-group-item"
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
}
export { RadioGroup, RadioGroupItem }

View File

@@ -340,14 +340,14 @@ export default function useDownloader() {
}
// Handle audio only
else if (fileType === 'audio' && (AUDIO_FORMAT !== 'auto' || format)) {
args.push('--extract-audio', '--audio-format', format || AUDIO_FORMAT);
args.push('--extract-audio', '--audio-format', format || AUDIO_FORMAT, '--audio-quality', '0');
}
// Handle unknown filetype
else if (fileType === 'unknown' && format) {
if (['mkv', 'mp4', 'webm'].includes(format)) {
args.push(recodeOrRemux, formatToUse);
} else if (['mp3', 'm4a', 'opus'].includes(format)) {
args.push('--extract-audio', '--audio-format', format);
args.push('--extract-audio', '--audio-format', format, '--audio-quality', '0');
}
}
}
@@ -365,6 +365,7 @@ export default function useDownloader() {
}
let embedThumbnail = 0;
let squareCropThumbnail = 0;
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || EMBED_VIDEO_THUMBNAIL || EMBED_AUDIO_THUMBNAIL)) {
const shouldEmbedThumbForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (EMBED_VIDEO_THUMBNAIL && downloadConfig.embed_thumbnail === null));
const shouldEmbedThumbForAudio = fileType === 'audio' && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (EMBED_AUDIO_THUMBNAIL && downloadConfig.embed_thumbnail === null));
@@ -372,7 +373,12 @@ export default function useDownloader() {
if (shouldEmbedThumbForUnknown || shouldEmbedThumbForVideo || shouldEmbedThumbForAudio) {
embedThumbnail = 1;
args.push('--embed-thumbnail');
args.push('--embed-thumbnail', '--convert-thumbnail', 'jpg');
if (downloadConfig.square_crop_thumbnail || resumeState?.square_crop_thumbnail) {
squareCropThumbnail = 1;
args.push('--postprocessor-args', 'ThumbnailsConvertor+FFmpeg_o:-c:v mjpeg -qmin 1 -qscale:v 1 -vf crop="\'min(iw,ih)\':\'min(iw,ih)\'"');
}
}
}
@@ -503,6 +509,7 @@ export default function useDownloader() {
output_format: outputFormat,
embed_metadata: embedMetadata,
embed_thumbnail: embedThumbnail,
square_crop_thumbnail: squareCropThumbnail,
sponsorblock_remove: sponsorblockRemove,
sponsorblock_mark: sponsorblockMark,
use_aria2: useAria2,
@@ -630,6 +637,7 @@ export default function useDownloader() {
output_format: resumeState?.output_format || null,
embed_metadata: resumeState?.embed_metadata || 0,
embed_thumbnail: resumeState?.embed_thumbnail || 0,
square_crop_thumbnail: resumeState?.square_crop_thumbnail || 0,
sponsorblock_remove: resumeState?.sponsorblock_remove || null,
sponsorblock_mark: resumeState?.sponsorblock_mark || null,
use_aria2: resumeState?.use_aria2 || 0,

View File

@@ -347,10 +347,8 @@ export default function DownloaderPage() {
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
allFilteredFormats={allFilteredFormats}
qualityPresetFormats={qualityPresetFormats}
subtitleLanguages={subtitleLanguages}
selectedFormat={selectedFormat}
/>
)}
{!isMetadataLoading && videoMetadata && selectedDownloadFormat && (

View File

@@ -101,12 +101,13 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
output_format,
embed_metadata,
embed_thumbnail,
square_crop_thumbnail,
sponsorblock_remove,
sponsorblock_mark,
use_aria2,
custom_command,
queue_config
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34)
ON CONFLICT(download_id) DO UPDATE SET
download_status = $2,
video_id = $3,
@@ -135,11 +136,12 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
output_format = $26,
embed_metadata = $27,
embed_thumbnail = $28,
sponsorblock_remove = $29,
sponsorblock_mark = $30,
use_aria2 = $31,
custom_command = $32,
queue_config = $33`,
square_crop_thumbnail = $29,
sponsorblock_remove = $30,
sponsorblock_mark = $31,
use_aria2 = $32,
custom_command = $33,
queue_config = $34`,
[
downloadState.download_id,
downloadState.download_status,
@@ -169,6 +171,7 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
downloadState.output_format,
downloadState.embed_metadata,
downloadState.embed_thumbnail,
downloadState.square_crop_thumbnail,
downloadState.sponsorblock_remove,
downloadState.sponsorblock_mark,
downloadState.use_aria2,

View File

@@ -58,6 +58,7 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
output_format: null,
embed_metadata: null,
embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null,
custom_command: null
},
@@ -86,6 +87,7 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
output_format: null,
embed_metadata: null,
embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null,
custom_command: null
}

View File

@@ -40,6 +40,7 @@ export interface DownloadState {
output_format: string | null;
embed_metadata: number;
embed_thumbnail: number;
square_crop_thumbnail: number;
sponsorblock_remove: string | null;
sponsorblock_mark: string | null;
use_aria2: number;
@@ -78,6 +79,7 @@ export interface Download {
output_format: string | null;
embed_metadata: number;
embed_thumbnail: number;
square_crop_thumbnail: number;
sponsorblock_remove: string | null;
sponsorblock_mark: string | null;
use_aria2: number;

View File

@@ -62,6 +62,7 @@ export interface DownloadConfiguration {
output_format: string | null;
embed_metadata: boolean | null;
embed_thumbnail: boolean | null;
square_crop_thumbnail: boolean | null;
sponsorblock: string | null;
custom_command: string | null;
}