feat: 支持多语言能力
This commit is contained in:
39
src/app/(dashboard)/tools/audio-compress/layout.tsx
Normal file
39
src/app/(dashboard)/tools/audio-compress/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Audio Compression - Compress & Convert Audio Files",
|
||||
zh: "音频压缩 - 压缩并转换音频文件",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Compress and convert audio files to various formats including MP3, AAC, OGG, FLAC. Adjust bitrate and sample rate for optimal quality.",
|
||||
zh: "压缩并转换音频文件为多种格式,包括 MP3、AAC、OGG、FLAC。调整比特率和采样率以获得最佳质量。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function AudioCompressLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, AudioCompressConfig } from "@/types";
|
||||
|
||||
const audioAccept = {
|
||||
@@ -23,59 +24,64 @@ const defaultConfig: AudioCompressConfig = {
|
||||
channels: 2,
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "bitrate",
|
||||
type: "select",
|
||||
label: "Bitrate",
|
||||
description: "Higher bitrate = better quality, larger file",
|
||||
value: defaultConfig.bitrate,
|
||||
options: [
|
||||
{ label: "64 kbps", value: 64 },
|
||||
{ label: "128 kbps", value: 128 },
|
||||
{ label: "192 kbps", value: 192 },
|
||||
{ label: "256 kbps", value: 256 },
|
||||
{ label: "320 kbps", value: 320 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Target audio format",
|
||||
value: defaultConfig.format,
|
||||
options: [
|
||||
{ label: "MP3", value: "mp3" },
|
||||
{ label: "AAC", value: "aac" },
|
||||
{ label: "OGG", value: "ogg" },
|
||||
{ label: "FLAC", value: "flac" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "sampleRate",
|
||||
type: "select",
|
||||
label: "Sample Rate",
|
||||
description: "Audio sample rate in Hz",
|
||||
value: defaultConfig.sampleRate,
|
||||
options: [
|
||||
{ label: "44.1 kHz", value: 44100 },
|
||||
{ label: "48 kHz", value: 48000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
type: "radio",
|
||||
label: "Channels",
|
||||
description: "Audio channels",
|
||||
value: defaultConfig.channels,
|
||||
options: [
|
||||
{ label: "Stereo (2 channels)", value: 2 },
|
||||
{ label: "Mono (1 channel)", value: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "bitrate",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.bitrate"),
|
||||
description: t("config.audioCompression.bitrateDescription"),
|
||||
value: config.bitrate,
|
||||
options: [
|
||||
{ label: "64 kbps", value: 64 },
|
||||
{ label: "128 kbps", value: 128 },
|
||||
{ label: "192 kbps", value: 192 },
|
||||
{ label: "256 kbps", value: 256 },
|
||||
{ label: "320 kbps", value: 320 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.format"),
|
||||
description: t("config.audioCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "MP3", value: "mp3" },
|
||||
{ label: "AAC", value: "aac" },
|
||||
{ label: "OGG", value: "ogg" },
|
||||
{ label: "FLAC", value: "flac" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "sampleRate",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.sampleRate"),
|
||||
description: t("config.audioCompression.sampleRateDescription"),
|
||||
value: config.sampleRate,
|
||||
options: [
|
||||
{ label: "44.1 kHz", value: 44100 },
|
||||
{ label: "48 kHz", value: 48000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
type: "radio",
|
||||
label: t("config.audioCompression.channels"),
|
||||
description: t("config.audioCompression.channelsDescription"),
|
||||
value: config.channels,
|
||||
options: [
|
||||
{ label: t("config.audioCompression.stereo"), value: 2 },
|
||||
{ label: t("config.audioCompression.mono"), value: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function AudioCompressPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -112,7 +118,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading audio...",
|
||||
message: t("processing.uploadingAudio"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -122,14 +128,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Compressing audio...",
|
||||
message: t("processing.compressingAudio"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -138,7 +144,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Compressing... ${i}%`,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,14 +168,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Compression complete!",
|
||||
message: t("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Compression failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -179,6 +185,7 @@ export default function AudioCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -192,9 +199,9 @@ export default function AudioCompressPage() {
|
||||
<Music className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Audio Compression</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.audioCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Compress and convert audio files with quality control
|
||||
{t("tools.audioCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,8 +220,8 @@ export default function AudioCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Audio Settings"
|
||||
description="Configure compression parameters"
|
||||
title={t("config.audioCompression.title")}
|
||||
description={t("config.audioCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof AudioCompressConfig],
|
||||
@@ -226,7 +233,7 @@ export default function AudioCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Volume2 className="mr-2 h-4 w-4" />
|
||||
Compress Audio
|
||||
{t("tools.audioCompression.compressAudio")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -241,15 +248,15 @@ export default function AudioCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">Supported Formats</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.audioCompression.supportedFormats")}</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Input</p>
|
||||
<p>MP3, WAV, OGG, AAC, FLAC, M4A</p>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.input")}</p>
|
||||
<p>{t("tools.audioCompression.inputFormats")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Output</p>
|
||||
<p>MP3, AAC, OGG, FLAC</p>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.output")}</p>
|
||||
<p>{t("tools.audioCompression.outputFormats")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
39
src/app/(dashboard)/tools/image-compress/layout.tsx
Normal file
39
src/app/(dashboard)/tools/image-compress/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Image Compression - Optimize Images for Web & Mobile",
|
||||
zh: "图片压缩 - 为网页和移动端优化图片",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Optimize images for web and mobile without quality loss. Support for batch processing and format conversion including PNG, JPEG, WebP.",
|
||||
zh: "为网页和移动端优化图片,不影响质量。支持批量处理和格式转换,包括 PNG、JPEG、WebP。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function ImageCompressLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, ImageCompressConfig } from "@/types";
|
||||
|
||||
const imageAccept = {
|
||||
@@ -21,35 +22,40 @@ const defaultConfig: ImageCompressConfig = {
|
||||
format: "original",
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: "Compression Quality",
|
||||
description: "Lower quality = smaller file size",
|
||||
value: defaultConfig.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
suffix: "%",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Convert to a different format (optional)",
|
||||
value: defaultConfig.format,
|
||||
options: [
|
||||
{ label: "Original", value: "original" },
|
||||
{ label: "JPEG", value: "jpeg" },
|
||||
{ label: "PNG", value: "png" },
|
||||
{ label: "WebP", value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
function useConfigOptions(config: ImageCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: t("config.imageCompression.quality"),
|
||||
description: t("config.imageCompression.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
suffix: "%",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.imageCompression.format"),
|
||||
description: t("config.imageCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: t("config.imageCompression.formatOriginal"), value: "original" },
|
||||
{ label: t("config.imageCompression.formatJpeg"), value: "jpeg" },
|
||||
{ label: t("config.imageCompression.formatPng"), value: "png" },
|
||||
{ label: t("config.imageCompression.formatWebp"), value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function ImageCompressPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -86,7 +92,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading images...",
|
||||
message: t("processing.uploadingImages"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -96,14 +102,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Compressing images...",
|
||||
message: t("processing.compressingImages"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -112,7 +118,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Compressing... ${i}%`,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,14 +141,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Compression complete!",
|
||||
message: t("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Compression failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -152,6 +158,7 @@ export default function ImageCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -165,9 +172,9 @@ export default function ImageCompressPage() {
|
||||
<ImageIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Image Compression</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.imageCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Optimize images for web and mobile without quality loss
|
||||
{t("tools.imageCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,8 +193,8 @@ export default function ImageCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Compression Settings"
|
||||
description="Configure compression options"
|
||||
title={t("config.imageCompression.title")}
|
||||
description={t("config.imageCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof ImageCompressConfig],
|
||||
@@ -199,7 +206,7 @@ export default function ImageCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Compress Images
|
||||
{t("tools.imageCompression.compressImages")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -214,12 +221,11 @@ export default function ImageCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">Features</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.imageCompression.features")}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>• Batch processing - compress multiple images at once</li>
|
||||
<li>• Smart compression - maintains visual quality</li>
|
||||
<li>• Format conversion - PNG to JPEG, WebP, and more</li>
|
||||
<li>• Up to 80% size reduction without quality loss</li>
|
||||
{(t("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
|
||||
<li key={index}>• {feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
39
src/app/(dashboard)/tools/video-frames/layout.tsx
Normal file
39
src/app/(dashboard)/tools/video-frames/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Video to Frames - Extract Frames from Videos",
|
||||
zh: "视频抽帧 - 从视频中提取帧",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Extract frames from videos with customizable frame rates. Perfect for sprite animations and game asset preparation. Supports MP4, MOV, AVI, WebM.",
|
||||
zh: "从视频中提取帧,可自定义帧率。非常适合精灵动画制作和游戏素材准备。支持 MP4、MOV、AVI、WebM。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function VideoFramesLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, VideoFramesConfig } from "@/types";
|
||||
|
||||
const videoAccept = {
|
||||
@@ -24,45 +25,50 @@ const defaultConfig: VideoFramesConfig = {
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "fps",
|
||||
type: "slider",
|
||||
label: "Frame Rate",
|
||||
description: "Number of frames to extract per second",
|
||||
value: defaultConfig.fps,
|
||||
min: 1,
|
||||
max: 60,
|
||||
step: 1,
|
||||
suffix: " fps",
|
||||
icon: <Video className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Image format for the extracted frames",
|
||||
value: defaultConfig.format,
|
||||
options: [
|
||||
{ label: "PNG", value: "png" },
|
||||
{ label: "JPEG", value: "jpeg" },
|
||||
{ label: "WebP", value: "webp" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: "Quality",
|
||||
description: "Image quality (for JPEG and WebP)",
|
||||
value: defaultConfig.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
suffix: "%",
|
||||
},
|
||||
];
|
||||
function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "fps",
|
||||
type: "slider",
|
||||
label: t("config.videoFrames.fps"),
|
||||
description: t("config.videoFrames.fpsDescription"),
|
||||
value: config.fps,
|
||||
min: 1,
|
||||
max: 60,
|
||||
step: 1,
|
||||
suffix: " fps",
|
||||
icon: <Video className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.videoFrames.format"),
|
||||
description: t("config.videoFrames.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "PNG", value: "png" },
|
||||
{ label: "JPEG", value: "jpeg" },
|
||||
{ label: "WebP", value: "webp" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: t("config.videoFrames.quality"),
|
||||
description: t("config.videoFrames.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
suffix: "%",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function VideoFramesPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -99,7 +105,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading video...",
|
||||
message: t("processing.uploadingVideo"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -109,14 +115,14 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Extracting frames...",
|
||||
message: t("processing.extractingFrames"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -125,7 +131,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Processing... ${i}%`,
|
||||
message: t("processing.processProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,24 +155,24 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Processing complete!",
|
||||
message: t("processing.processingComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Processing failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.processingFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = (fileId: string) => {
|
||||
console.log("Downloading file:", fileId);
|
||||
// Implement download logic
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -181,9 +187,9 @@ export default function VideoFramesPage() {
|
||||
<Video className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Video to Frames</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.videoFrames.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Extract frames from videos with customizable settings
|
||||
{t("tools.videoFrames.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,8 +209,8 @@ export default function VideoFramesPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Export Settings"
|
||||
description="Configure how frames are extracted"
|
||||
title={t("config.videoFrames.title")}
|
||||
description={t("config.videoFrames.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof VideoFramesConfig],
|
||||
@@ -216,7 +222,7 @@ export default function VideoFramesPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Process Video
|
||||
{t("tools.videoFrames.processVideo")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -233,12 +239,11 @@ export default function VideoFramesPage() {
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">How it works</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.videoFrames.howItWorks")}</h3>
|
||||
<ol className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>1. Upload your video file (MP4, MOV, AVI, etc.)</li>
|
||||
<li>2. Configure frame rate, format, and quality</li>
|
||||
<li>3. Click "Process Video" to start extraction</li>
|
||||
<li>4. Download the ZIP file with all frames</li>
|
||||
{(t("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
|
||||
<li key={index}>{index + 1}. {step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user