Reverted all changes back to version eb7e8cb60d
This commit is contained in:
@@ -3,28 +3,17 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useI18n } from "@/lib/i18n/client";
|
||||
import { changelogData, type Change } from "@/lib/changelog-config";
|
||||
import { changelogData } from "@/lib/changelog-data";
|
||||
|
||||
export function Changelog() {
|
||||
const t = useI18n();
|
||||
|
||||
const getBadgeText = (type: Change['type']) => {
|
||||
switch (type) {
|
||||
case 'New': return t('changelog.types.new');
|
||||
case 'Improved': return t('changelog.types.improved');
|
||||
case 'Fixed': return t('changelog.types.fixed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
||||
{t('changelog.title')}
|
||||
Changelog
|
||||
</h1>
|
||||
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
|
||||
{t('changelog.subtitle')}
|
||||
Tracking all the new features, improvements, and bug fixes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
@@ -32,7 +21,7 @@ export function Changelog() {
|
||||
<Card key={entry.version}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<CardTitle className="text-2xl font-bold">{t('changelog.version')} {entry.version}</CardTitle>
|
||||
<CardTitle className="text-2xl font-bold">Version {entry.version}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{entry.date}</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -48,9 +37,9 @@ export function Changelog() {
|
||||
"border-red-500/50 bg-red-500/10 text-red-700 dark:text-red-300": change.type === "Fixed",
|
||||
})}
|
||||
>
|
||||
{getBadgeText(change.type)}
|
||||
{change.type}
|
||||
</Badge>
|
||||
<p className="text-foreground">{t(`changelog.logs.${change.textKey}`)}</p>
|
||||
<p className="text-foreground">{change.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,44 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Github, Twitter, Globe } from "lucide-react";
|
||||
import { Github, Twitter } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { changelogData } from "@/lib/changelog-config";
|
||||
import { useI18n, useChangeLocale } from "@/lib/i18n/client";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { changelogData } from "@/lib/changelog-data";
|
||||
|
||||
export function Footer() {
|
||||
const t = useI18n();
|
||||
const changeLocale = useChangeLocale();
|
||||
const latestVersion = changelogData[0]?.version;
|
||||
|
||||
return (
|
||||
<footer className="w-full border-t bg-background">
|
||||
<div className="container relative mx-auto flex h-16 items-center justify-between px-4 md:px-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="Change language">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => changeLocale("en")}>
|
||||
English
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => changeLocale("de")}>
|
||||
Deutsch
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="text-sm text-muted-foreground hidden sm:block">
|
||||
<p>© {new Date().getFullYear()} Pascal Linxweiler</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>© {new Date().getFullYear()} Pascal Linxweiler</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1">
|
||||
@@ -55,8 +27,8 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<Link href="/imprint" className="hover:text-primary transition-colors">{t('footer.imprint')}</Link>
|
||||
<Link href="/privacy" className="hover:text-primary transition-colors">{t('footer.privacy')}</Link>
|
||||
<Link href="/imprint" className="hover:text-primary transition-colors">Imprint</Link>
|
||||
<Link href="/privacy" className="hover:text-primary transition-colors">Privacy</Link>
|
||||
{latestVersion && (
|
||||
<Link
|
||||
href="/changelog"
|
||||
|
||||
@@ -36,7 +36,14 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useScopedI18n } from "@/lib/i18n/client";
|
||||
|
||||
const aspectRatios = [
|
||||
{ name: "Custom", value: "custom" },
|
||||
{ name: "1:1 (Square)", value: "1/1" },
|
||||
{ name: "4:3 (Standard)", value: "4/3" },
|
||||
{ name: "3:2 (Photography)", value: "3/2" },
|
||||
{ name: "16:9 (Widescreen)", value: "16/9" },
|
||||
];
|
||||
|
||||
const initialSettings = {
|
||||
width: "",
|
||||
@@ -57,16 +64,6 @@ const initialSettings = {
|
||||
};
|
||||
|
||||
export function ImageConverter() {
|
||||
const t = useScopedI18n('converter');
|
||||
|
||||
const aspectRatios = [
|
||||
{ name: t('aspect_ratios.custom'), value: "custom" },
|
||||
{ name: t('aspect_ratios.square'), value: "1/1" },
|
||||
{ name: t('aspect_ratios.standard'), value: "4/3" },
|
||||
{ name: t('aspect_ratios.photography'), value: "3/2" },
|
||||
{ name: t('aspect_ratios.widescreen'), value: "16/9" },
|
||||
];
|
||||
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
|
||||
const [filenames, setFilenames] = useState<string[]>([]);
|
||||
@@ -107,7 +104,7 @@ export function ImageConverter() {
|
||||
);
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
toast.error(t('toasts.no_valid_files'));
|
||||
toast.error("No valid image files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,7 +126,7 @@ export function ImageConverter() {
|
||||
setPreviewUrls(newPreviewUrls);
|
||||
setFilenames(newFilenames);
|
||||
|
||||
toast.success(t('toasts.images_added', { count: imageFiles.length }));
|
||||
toast.success(`${imageFiles.length} image(s) added.`);
|
||||
};
|
||||
|
||||
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -171,7 +168,7 @@ export function ImageConverter() {
|
||||
setFilenames([]);
|
||||
setWidth(initialSettings.width);
|
||||
setHeight(initialSettings.height);
|
||||
toast.info(t('toasts.all_cleared'));
|
||||
toast.info("All images cleared.");
|
||||
};
|
||||
|
||||
const handleFilenameChange = (index: number, newName: string) => {
|
||||
@@ -289,23 +286,23 @@ export function ImageConverter() {
|
||||
document.body.removeChild(link);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(t('toasts.error_processing', { filename: image.name })));
|
||||
reject(new Error(`Could not process ${image.name}.`));
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
reject(new Error(t('toasts.error_loading', { filename: image.name })));
|
||||
reject(new Error(`Failed to load ${image.name} for conversion.`));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleConvertAndDownloadAll = async () => {
|
||||
if (images.length === 0) {
|
||||
toast.error(t('toasts.upload_images_first'));
|
||||
toast.error("Please upload images first.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConverting(true);
|
||||
toast.info(t('toasts.conversion_start_all', { count: images.length }));
|
||||
toast.info(`Starting conversion for ${images.length} images...`);
|
||||
|
||||
const conversionPromises = images.map((image, index) =>
|
||||
convertAndDownload(image, previewUrls[index], index)
|
||||
@@ -313,12 +310,12 @@ export function ImageConverter() {
|
||||
|
||||
try {
|
||||
await Promise.all(conversionPromises);
|
||||
toast.success(t('toasts.conversion_success_all', { count: images.length }));
|
||||
toast.success(`Successfully exported all ${images.length} images!`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error(t('toasts.conversion_error'));
|
||||
toast.error("An unknown error occurred during conversion.");
|
||||
}
|
||||
} finally {
|
||||
setIsConverting(false);
|
||||
@@ -327,16 +324,16 @@ export function ImageConverter() {
|
||||
|
||||
const handleConvertAndDownloadSingle = async (index: number) => {
|
||||
setConvertingIndex(index);
|
||||
toast.info(t('toasts.conversion_start_single', { filename: filenames[index] }));
|
||||
toast.info(`Starting conversion for ${filenames[index]}...`);
|
||||
|
||||
try {
|
||||
await convertAndDownload(images[index], previewUrls[index], index);
|
||||
toast.success(t('toasts.conversion_success_single', { filename: filenames[index] }));
|
||||
toast.success(`Successfully exported ${filenames[index]}!`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error(t('toasts.conversion_error'));
|
||||
toast.error("An unknown error occurred during conversion.");
|
||||
}
|
||||
} finally {
|
||||
setConvertingIndex(null);
|
||||
@@ -344,7 +341,7 @@ export function ImageConverter() {
|
||||
};
|
||||
|
||||
const handleApplySettings = () => {
|
||||
toast.info(t('toasts.settings_applied'));
|
||||
toast.info("Settings updated and will be used for all downloads.");
|
||||
};
|
||||
|
||||
const handleResetSettings = () => {
|
||||
@@ -363,7 +360,7 @@ export function ImageConverter() {
|
||||
setDefaultBaseName(initialSettings.defaultBaseName);
|
||||
setScaleMode(initialSettings.scaleMode);
|
||||
setObjectPosition(initialSettings.objectPosition);
|
||||
toast.success(t('toasts.settings_reset'));
|
||||
toast.success("All settings have been reset to their defaults.");
|
||||
};
|
||||
|
||||
const handleAspectRatioChange = (value: string) => {
|
||||
@@ -411,16 +408,16 @@ export function ImageConverter() {
|
||||
|
||||
const handleApplyDefaultBaseNameToAll = () => {
|
||||
if (!defaultBaseName) {
|
||||
toast.error(t('toasts.base_name_required'));
|
||||
toast.error("Please enter a default base name to apply.");
|
||||
return;
|
||||
}
|
||||
if (!hasImages) {
|
||||
toast.info(t('toasts.upload_first'));
|
||||
toast.info("Upload some images first.");
|
||||
return;
|
||||
}
|
||||
const newFilenames = filenames.map(() => defaultBaseName);
|
||||
setFilenames(newFilenames);
|
||||
toast.success(t('toasts.base_name_applied', { baseName: defaultBaseName, count: images.length }));
|
||||
toast.success(`Set base name to "${defaultBaseName}" for all ${images.length} images.`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -430,7 +427,7 @@ export function ImageConverter() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">{t('upload.title')}</h3>
|
||||
<h3 className="text-lg font-medium">Upload Images</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
||||
@@ -443,8 +440,8 @@ export function ImageConverter() {
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
||||
<Upload className="w-8 h-8 mb-2" />
|
||||
<p className="font-semibold">{t('upload.cta')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('upload.supported')}</p>
|
||||
<p className="font-semibold">Click or drag and drop to upload</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
||||
</div>
|
||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||
</div>
|
||||
@@ -456,25 +453,25 @@ export function ImageConverter() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>{t('uploaded.title')}</CardTitle>
|
||||
<CardTitle>Uploaded Images</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />{t('uploaded.clear_all')}</Button>
|
||||
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('uploaded.clear_all_tooltip')}</p>
|
||||
<p>Remove all uploaded images.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{isConverting ? t('uploaded.converting') : t('uploaded.download_all', { count: images.length })}
|
||||
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('uploaded.download_all_tooltip')}</p>
|
||||
<p>Convert and download all images with the current settings.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -488,7 +485,7 @@ export function ImageConverter() {
|
||||
<div key={url} className="p-4 border rounded-lg flex items-center gap-4">
|
||||
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t('uploaded.base_name')}</Label>
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
||||
<Input
|
||||
id={`filename-${index}`}
|
||||
value={filenames[index]}
|
||||
@@ -496,7 +493,7 @@ export function ImageConverter() {
|
||||
className="text-sm font-medium h-8 mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
|
||||
{t('uploaded.final_name', { filename: `${finalFilename}.${format}` })}
|
||||
Final name: {finalFilename}.{format}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center shrink-0">
|
||||
@@ -513,7 +510,7 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('uploaded.download_image_tooltip')}</p>
|
||||
<p>Download this image</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -529,7 +526,7 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('uploaded.remove_image_tooltip')}</p>
|
||||
<p>Remove this image</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -547,9 +544,9 @@ export function ImageConverter() {
|
||||
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">{t('settings.image.title')}</h3>
|
||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('settings.image.description')}
|
||||
Adjust resolution and scaling for all images.
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -557,13 +554,13 @@ export function ImageConverter() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="aspect-ratio">{t('settings.image.aspect_ratio')}</Label>
|
||||
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.aspect_ratio_tooltip')}</p>
|
||||
<p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -583,13 +580,13 @@ export function ImageConverter() {
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="width">{t('settings.image.width')}</Label>
|
||||
<Label htmlFor="width">Width (px)</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.width_tooltip')}</p>
|
||||
<p>Set the output width in pixels. Leave blank to use the original width.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -602,18 +599,18 @@ export function ImageConverter() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.swap_tooltip')}</p>
|
||||
<p>Swap the entered width and height values.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="height">{t('settings.image.height')}</Label>
|
||||
<Label htmlFor="height">Height (px)</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.height_tooltip')}</p>
|
||||
<p>Set the output height in pixels. Leave blank to use the original height.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -623,13 +620,13 @@ export function ImageConverter() {
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
|
||||
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
|
||||
{t('settings.image.keep_orientation')}
|
||||
Keep original orientation
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.keep_orientation_tooltip')}</p>
|
||||
<p>Automatically swaps width and height to match the original image's orientation.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
@@ -637,39 +634,39 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="scale-mode">{t('settings.image.scaling')}</Label>
|
||||
<Label htmlFor="scale-mode">Scaling</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.scaling_tooltip')}</p>
|
||||
<p>Determines how the image fits into the new dimensions.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
|
||||
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fill">{t('settings.image.scaling_options.fill')}</SelectItem>
|
||||
<SelectItem value="cover">{t('settings.image.scaling_options.cover')}</SelectItem>
|
||||
<SelectItem value="contain">{t('settings.image.scaling_options.contain')}</SelectItem>
|
||||
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
||||
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
||||
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{scaleMode !== 'fill' && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label>{t('settings.image.position')}</Label>
|
||||
<Label>Position</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.image.position_tooltip')}</p>
|
||||
<p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos as string)} />
|
||||
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos)} />
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
@@ -678,8 +675,8 @@ export function ImageConverter() {
|
||||
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">{t('settings.filename.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('settings.filename.description')}</p>
|
||||
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
@@ -687,20 +684,20 @@ export function ImageConverter() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
|
||||
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
|
||||
{t('settings.filename.use_default_base_name')}
|
||||
Use default base name
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.use_default_base_name_tooltip')}</p>
|
||||
<p>When enabled, all newly uploaded images will use the specified default base name.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
{useDefaultBaseName && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-base-name">{t('settings.filename.default_base_name')}</Label>
|
||||
<Label htmlFor="default-base-name">Default base name</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="default-base-name"
|
||||
@@ -711,11 +708,11 @@ export function ImageConverter() {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
|
||||
{t('settings.filename.apply_to_all')}
|
||||
Apply to all
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.apply_to_all_tooltip')}</p>
|
||||
<p>Apply this base name to all currently uploaded images.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -723,13 +720,13 @@ export function ImageConverter() {
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="prefix">{t('settings.filename.prefix')}</Label>
|
||||
<Label htmlFor="prefix">Prefix</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.prefix_tooltip')}</p>
|
||||
<p>Add text to the beginning of every filename.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -737,13 +734,13 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="suffix">{t('settings.filename.suffix')}</Label>
|
||||
<Label htmlFor="suffix">Suffix</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.suffix_tooltip')}</p>
|
||||
<p>Add text to the end of every filename (before the number).</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -752,13 +749,13 @@ export function ImageConverter() {
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
|
||||
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
|
||||
{t('settings.filename.add_sequential_number')}
|
||||
Add sequential number
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.add_sequential_number_tooltip')}</p>
|
||||
<p>Append a numbered sequence to each filename.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
@@ -767,13 +764,13 @@ export function ImageConverter() {
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-start">{t('settings.filename.start_number')}</Label>
|
||||
<Label htmlFor="counter-start">Start number</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.start_number_tooltip')}</p>
|
||||
<p>The first number to use in the sequence.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -787,13 +784,13 @@ export function ImageConverter() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-digits">{t('settings.filename.padding_digits')}</Label>
|
||||
<Label htmlFor="counter-digits">Padding digits</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.filename.padding_digits_tooltip')}</p>
|
||||
<p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -814,21 +811,21 @@ export function ImageConverter() {
|
||||
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">{t('settings.quality.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('settings.quality.description')}</p>
|
||||
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="format">{t('settings.quality.format')}</Label>
|
||||
<Label htmlFor="format">Format</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.quality.format_tooltip')}</p>
|
||||
<p>Choose the output file format for the images.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -844,13 +841,13 @@ export function ImageConverter() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="quality">{t('settings.quality.quality')}</Label>
|
||||
<Label htmlFor="quality">Quality</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.quality.quality_tooltip')}</p>
|
||||
<p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -866,7 +863,7 @@ export function ImageConverter() {
|
||||
disabled={format === 'png'}
|
||||
/>
|
||||
{format === 'png' && (
|
||||
<p className="text-xs text-muted-foreground pt-1">{t('settings.quality.png_disabled_notice')}</p>
|
||||
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,11 +879,11 @@ export function ImageConverter() {
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{t('settings.reset')}
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.reset_tooltip')}</p>
|
||||
<p>Reset all settings to their default values.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -896,11 +893,11 @@ export function ImageConverter() {
|
||||
className="w-full"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{t('settings.apply')}
|
||||
Apply
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('settings.apply_tooltip')}</p>
|
||||
<p>Confirm and apply all the settings above. This does not download the images.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user