mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(tools)!: SnapOtter 2.0 phase 4 wave 1: 45 core tools across all modalities (#219)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type Preset = "screen" | "ebook" | "printer";
|
||||
|
||||
export function CompressPdfSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["compress-pdf"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("compress-pdf");
|
||||
|
||||
const [preset, setPreset] = useState<Preset>("ebook");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { preset };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="cpdf-preset" className="text-xs text-muted-foreground">
|
||||
{s.preset}
|
||||
</label>
|
||||
<select
|
||||
id="cpdf-preset"
|
||||
value={preset}
|
||||
onChange={(e) => setPreset(e.target.value as Preset)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="screen">{s.screen}</option>
|
||||
<option value="ebook">{s.ebook}</option>
|
||||
<option value="printer">{s.printer}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compress-pdf-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type Quality = "light" | "balanced" | "strong";
|
||||
type Resolution = "original" | "1080p" | "720p" | "480p";
|
||||
|
||||
export function CompressVideoSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["compress-video"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("compress-video");
|
||||
|
||||
const [quality, setQuality] = useState<Quality>("balanced");
|
||||
const [resolution, setResolution] = useState<Resolution>("original");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { quality, resolution };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="cpv-quality" className="text-xs text-muted-foreground">
|
||||
{s.quality}
|
||||
</label>
|
||||
<select
|
||||
id="cpv-quality"
|
||||
value={quality}
|
||||
onChange={(e) => setQuality(e.target.value as Quality)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="light">{s.light}</option>
|
||||
<option value="balanced">{s.balanced}</option>
|
||||
<option value="strong">{s.strong}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="cpv-resolution" className="text-xs text-muted-foreground">
|
||||
{s.resolution}
|
||||
</label>
|
||||
<select
|
||||
id="cpv-resolution"
|
||||
value={resolution}
|
||||
onChange={(e) => setResolution(e.target.value as Resolution)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="original">{s.original}</option>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
<option value="480p">480p</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compress-video-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type AudioFormat = "mp3" | "wav" | "ogg" | "flac" | "m4a";
|
||||
|
||||
const BITRATE_OPTIONS = [96, 128, 192, 256, 320] as const;
|
||||
|
||||
export function ConvertAudioSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["convert-audio"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("convert-audio");
|
||||
|
||||
const [outFormat, setOutFormat] = useState<AudioFormat>("mp3");
|
||||
const [bitrateKbps, setBitrateKbps] = useState(192);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { format: outFormat, bitrateKbps };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="ca-format" className="text-xs text-muted-foreground">
|
||||
{s.format}
|
||||
</label>
|
||||
<select
|
||||
id="ca-format"
|
||||
value={outFormat}
|
||||
onChange={(e) => setOutFormat(e.target.value as AudioFormat)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="mp3">MP3</option>
|
||||
<option value="wav">WAV</option>
|
||||
<option value="ogg">OGG</option>
|
||||
<option value="flac">FLAC</option>
|
||||
<option value="m4a">M4A</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="ca-bitrate" className="text-xs text-muted-foreground">
|
||||
{s.bitrate}
|
||||
</label>
|
||||
<select
|
||||
id="ca-bitrate"
|
||||
value={bitrateKbps}
|
||||
onChange={(e) => setBitrateKbps(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
{BITRATE_OPTIONS.map((br) => (
|
||||
<option key={br} value={br}>
|
||||
{br} kbps
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="convert-audio-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type VideoFormat = "mp4" | "mov" | "webm";
|
||||
type Quality = "high" | "balanced" | "small";
|
||||
|
||||
export function ConvertVideoSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["convert-video"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("convert-video");
|
||||
|
||||
const [outFormat, setOutFormat] = useState<VideoFormat>("mp4");
|
||||
const [quality, setQuality] = useState<Quality>("balanced");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { format: outFormat, quality };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="cv-format" className="text-xs text-muted-foreground">
|
||||
{s.format}
|
||||
</label>
|
||||
<select
|
||||
id="cv-format"
|
||||
value={outFormat}
|
||||
onChange={(e) => setOutFormat(e.target.value as VideoFormat)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="mov">MOV</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="cv-quality" className="text-xs text-muted-foreground">
|
||||
{s.quality}
|
||||
</label>
|
||||
<select
|
||||
id="cv-quality"
|
||||
value={quality}
|
||||
onChange={(e) => setQuality(e.target.value as Quality)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="high">{s.high}</option>
|
||||
<option value="balanced">{s.balanced}</option>
|
||||
<option value="small">{s.small}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="convert-video-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function CsvExcelSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["csv-excel"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("csv-excel");
|
||||
|
||||
const [sheet, setSheet] = useState(1);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const firstFileName = files[0]?.name ?? "";
|
||||
const isXlsx = firstFileName.toLowerCase().endsWith(".xlsx");
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = isXlsx ? { sheet } : {};
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isXlsx && (
|
||||
<div>
|
||||
<label htmlFor="ce-sheet" className="text-xs text-muted-foreground">
|
||||
{s.sheet}
|
||||
</label>
|
||||
<input
|
||||
id="ce-sheet"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={sheet}
|
||||
onChange={(e) => setSheet(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="csv-excel-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function CsvJsonSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["csv-json"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("csv-json");
|
||||
|
||||
const [pretty, setPretty] = useState(true);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { pretty };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pretty}
|
||||
onChange={(e) => setPretty(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
{s.pretty}
|
||||
</label>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="csv-json-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type AudioFormat = "mp3" | "wav" | "m4a";
|
||||
|
||||
export function ExtractAudioSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["extract-audio"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("extract-audio");
|
||||
|
||||
const [outFormat, setOutFormat] = useState<AudioFormat>("mp3");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { format: outFormat };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="ea-format" className="text-xs text-muted-foreground">
|
||||
{s.format}
|
||||
</label>
|
||||
<select
|
||||
id="ea-format"
|
||||
value={outFormat}
|
||||
onChange={(e) => setOutFormat(e.target.value as AudioFormat)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="mp3">MP3</option>
|
||||
<option value="wav">WAV</option>
|
||||
<option value="m4a">M4A</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="extract-audio-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function JsonXmlSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["json-xml"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("json-xml");
|
||||
|
||||
const [pretty, setPretty] = useState(true);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { pretty };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pretty}
|
||||
onChange={(e) => setPretty(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
{s.pretty}
|
||||
</label>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="json-xml-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function MergePdfSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["merge-pdf"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processing, error, progress } = useToolProcessor("merge-pdf");
|
||||
|
||||
const hasEnough = files.length >= 2;
|
||||
|
||||
const handleProcess = () => {
|
||||
processFiles(files, {});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{files.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{hasEnough ? format(s.orderHint, { count: files.length }) : s.needTwo}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="merge-pdf-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasEnough || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function MuteVideoSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["mute-video"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("mute-video");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = {};
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mute-video-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type Angle = 90 | 180 | 270;
|
||||
|
||||
export function RotatePdfSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["rotate-pdf"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("rotate-pdf");
|
||||
|
||||
const [angle, setAngle] = useState<Angle>(90);
|
||||
const [range, setRange] = useState("1-z");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { angle, range };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="rpdf-angle" className="text-xs text-muted-foreground">
|
||||
{s.angle}
|
||||
</label>
|
||||
<select
|
||||
id="rpdf-angle"
|
||||
value={angle}
|
||||
onChange={(e) => setAngle(Number(e.target.value) as Angle)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value={90}>90</option>
|
||||
<option value={180}>180</option>
|
||||
<option value={270}>270</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="rpdf-range" className="text-xs text-muted-foreground">
|
||||
{s.range}
|
||||
</label>
|
||||
<input
|
||||
id="rpdf-range"
|
||||
type="text"
|
||||
value={range}
|
||||
onChange={(e) => setRange(e.target.value)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">{s.rangeHint}</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rotate-pdf-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function SplitCsvSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["split-csv"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("split-csv");
|
||||
|
||||
const [rowsPerFile, setRowsPerFile] = useState(1000);
|
||||
const [keepHeader, setKeepHeader] = useState(true);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { rowsPerFile, keepHeader };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="sc-rows" className="text-xs text-muted-foreground">
|
||||
{s.rowsPerFile}
|
||||
</label>
|
||||
<input
|
||||
id="sc-rows"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={rowsPerFile}
|
||||
onChange={(e) => setRowsPerFile(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={keepHeader}
|
||||
onChange={(e) => setKeepHeader(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
{s.keepHeader}
|
||||
</label>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="split-csv-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type SplitMode = "range" | "every";
|
||||
|
||||
export function SplitPdfSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["split-pdf"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("split-pdf");
|
||||
|
||||
const [mode, setMode] = useState<SplitMode>("range");
|
||||
const [range, setRange] = useState("1-3,5");
|
||||
const [everyN, setEveryN] = useState(1);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = mode === "range" ? { mode, range } : { mode, everyN };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="sp-mode" className="text-xs text-muted-foreground">
|
||||
{s.mode}
|
||||
</label>
|
||||
<select
|
||||
id="sp-mode"
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value as SplitMode)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="range">{s.range}</option>
|
||||
<option value="every">{s.everyN}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{mode === "range" && (
|
||||
<div>
|
||||
<label htmlFor="sp-range" className="text-xs text-muted-foreground">
|
||||
{s.range}
|
||||
</label>
|
||||
<input
|
||||
id="sp-range"
|
||||
type="text"
|
||||
value={range}
|
||||
placeholder="1-3,5"
|
||||
onChange={(e) => setRange(e.target.value)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">{s.rangeHint}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "every" && (
|
||||
<div>
|
||||
<label htmlFor="sp-every" className="text-xs text-muted-foreground">
|
||||
{s.everyN}
|
||||
</label>
|
||||
<input
|
||||
id="sp-every"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={everyN}
|
||||
onChange={(e) => setEveryN(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="split-pdf-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function TrimAudioSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["trim-audio"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("trim-audio");
|
||||
|
||||
const [startS, setStartS] = useState(0);
|
||||
const [endS, setEndS] = useState(10);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
const rangeValid = endS > startS;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { startS, endS };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="ta-start" className="text-xs text-muted-foreground">
|
||||
{s.start}
|
||||
</label>
|
||||
<input
|
||||
id="ta-start"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={startS}
|
||||
onChange={(e) => setStartS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="ta-end" className="text-xs text-muted-foreground">
|
||||
{s.end}
|
||||
</label>
|
||||
<input
|
||||
id="ta-end"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={endS}
|
||||
onChange={(e) => setEndS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
{!rangeValid && (
|
||||
<p className="text-[10px] text-amber-500 mt-0.5">End time must be after start time</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="trim-audio-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing || !rangeValid}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function TrimVideoSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["trim-video"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("trim-video");
|
||||
|
||||
const [startS, setStartS] = useState(0);
|
||||
const [endS, setEndS] = useState(10);
|
||||
const [precise, setPrecise] = useState(false);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
const rangeValid = endS > startS;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { startS, endS, precise };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="tv-start" className="text-xs text-muted-foreground">
|
||||
{s.start}
|
||||
</label>
|
||||
<input
|
||||
id="tv-start"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={startS}
|
||||
onChange={(e) => setStartS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="tv-end" className="text-xs text-muted-foreground">
|
||||
{s.end}
|
||||
</label>
|
||||
<input
|
||||
id="tv-end"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={endS}
|
||||
onChange={(e) => setEndS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
{!rangeValid && (
|
||||
<p className="text-[10px] text-amber-500 mt-0.5">End time must be after start time</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={precise}
|
||||
onChange={(e) => setPrecise(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
{s.precise}
|
||||
</label>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="trim-video-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing || !rangeValid}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function VideoToGifSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["video-to-gif"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("video-to-gif");
|
||||
|
||||
const [fps, setFps] = useState(12);
|
||||
const [width, setWidth] = useState(480);
|
||||
const [startS, setStartS] = useState(0);
|
||||
const [durationS, setDurationS] = useState(5);
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { fps, width, startS, durationS };
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="vtg-fps" className="text-xs text-muted-foreground">
|
||||
{s.fps}
|
||||
</label>
|
||||
<input
|
||||
id="vtg-fps"
|
||||
type="number"
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
value={fps}
|
||||
onChange={(e) => setFps(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="vtg-width" className="text-xs text-muted-foreground">
|
||||
{s.width}
|
||||
</label>
|
||||
<input
|
||||
id="vtg-width"
|
||||
type="number"
|
||||
min={64}
|
||||
max={1280}
|
||||
step={1}
|
||||
value={width}
|
||||
onChange={(e) => setWidth(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="vtg-start" className="text-xs text-muted-foreground">
|
||||
{s.start}
|
||||
</label>
|
||||
<input
|
||||
id="vtg-start"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={startS}
|
||||
onChange={(e) => setStartS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="vtg-duration" className="text-xs text-muted-foreground">
|
||||
{s.duration}
|
||||
</label>
|
||||
<input
|
||||
id="vtg-duration"
|
||||
type="number"
|
||||
min={0.1}
|
||||
max={60}
|
||||
step={0.1}
|
||||
value={durationS}
|
||||
onChange={(e) => setDurationS(Number(e.target.value))}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="video-to-gif-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function WordToPdfSettings() {
|
||||
const { t } = useTranslation();
|
||||
const s = t.toolSettings["word-to-pdf"];
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("word-to-pdf");
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
const hasMultiple = files.length > 1;
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = {};
|
||||
if (hasMultiple) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label={s.progressLabel}
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="word-to-pdf-submit"
|
||||
onClick={handleProcess}
|
||||
disabled={!hasFile || processing}
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{hasMultiple ? format(s.submitBatch, { count: files.length }) : s.submit}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user