mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: harden Docker image and async job responses
Harden Docker runtime packaging, preserve async job response semantics, fix Redis subscriber startup connections, clear lint warnings, and harden enterprise S3 object body handling.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { AlertCircle, FileImage, FileUp, Upload } from "lucide-react";
|
||||
import { type DragEvent, useCallback, useEffect, useState } from "react";
|
||||
import { type DragEvent, type KeyboardEvent, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useUrlImport } from "@/hooks/use-url-import";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -177,7 +177,7 @@ export function Dropzone({
|
||||
[onFiles, checkFile, acceptDescription, accept],
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = useCallback(() => {
|
||||
setError(null);
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
@@ -193,7 +193,17 @@ export function Dropzone({
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
}, [multiple, resolvedAccept, checkFile, onFiles, acceptDescription, accept]);
|
||||
|
||||
const handleDropzoneKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLElement>) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
},
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
@@ -234,6 +244,7 @@ export function Dropzone({
|
||||
onDragLeave={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleDropzoneKeyDown}
|
||||
className={cn(
|
||||
"group flex flex-col items-center justify-center rounded-2xl border-2 border-dashed transition-all duration-200 mx-auto max-w-2xl w-full cursor-pointer",
|
||||
compact ? "min-h-0 h-full" : "min-h-[400px]",
|
||||
|
||||
@@ -94,10 +94,7 @@ export function UrlImportModal({ onClose, onImport }: UrlImportModalProps) {
|
||||
}, [handleClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Overlay */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -420,7 +420,7 @@ function GeneralSection() {
|
||||
setSaving(false);
|
||||
setTimeout(() => setSaveMsg(null), 3000);
|
||||
}
|
||||
}, [defaultToolView]);
|
||||
}, [defaultToolView, t.settings.general.saveSuccess, t.settings.general.saveFailed]);
|
||||
|
||||
const username = user?.username || "admin";
|
||||
const role = user?.role || "unknown";
|
||||
@@ -940,7 +940,16 @@ function SecuritySection() {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[currentPassword, newPassword, confirmPassword],
|
||||
[
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
t.settings.security.changeFailed,
|
||||
t.settings.security.currentPasswordIncorrect,
|
||||
t.settings.security.changeSuccess,
|
||||
t.settings.security.passwordsMismatch,
|
||||
t.settings.security.passwordTooShort,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -1468,7 +1477,17 @@ function PeopleSection() {
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
}
|
||||
},
|
||||
[newUsername, newPassword, newRole, newTeam, maxUsers, loadUsers],
|
||||
[
|
||||
newUsername,
|
||||
newPassword,
|
||||
newRole,
|
||||
newTeam,
|
||||
maxUsers,
|
||||
loadUsers,
|
||||
t.settings.people.createFailed,
|
||||
t.settings.people.createSuccess,
|
||||
t.settings.people.userLimitReached,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDeleteUser = useCallback(
|
||||
@@ -1487,7 +1506,12 @@ function PeopleSection() {
|
||||
setOpenMenuId(null);
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[loadUsers],
|
||||
[
|
||||
loadUsers,
|
||||
t.settings.people.deleteSuccess,
|
||||
t.settings.people.deleteFailed,
|
||||
t.settings.people.deleteConfirm,
|
||||
],
|
||||
);
|
||||
|
||||
const handleUpdateUser = useCallback(
|
||||
@@ -1511,7 +1535,14 @@ function PeopleSection() {
|
||||
}
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[editingUser, editRole, editTeam, loadUsers],
|
||||
[
|
||||
editingUser,
|
||||
editRole,
|
||||
editTeam,
|
||||
loadUsers,
|
||||
t.settings.people.cannotRemoveOwnAdmin,
|
||||
t.settings.people.updateSuccess,
|
||||
],
|
||||
);
|
||||
|
||||
const handleResetPassword = useCallback(
|
||||
@@ -1531,7 +1562,7 @@ function PeopleSection() {
|
||||
}
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[resetPasswordUser, resetPassword],
|
||||
[resetPasswordUser, resetPassword, t.settings.people.resetSuccess],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -2357,7 +2388,7 @@ function TeamsSection() {
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
}
|
||||
},
|
||||
[newTeamName, loadTeams],
|
||||
[newTeamName, loadTeams, t.settings.teams.duplicateName, t.settings.teams.createSuccess],
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
@@ -2375,7 +2406,7 @@ function TeamsSection() {
|
||||
}
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[editingTeamName, loadTeams],
|
||||
[editingTeamName, loadTeams, t.settings.teams.renameSuccess],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@@ -2395,7 +2426,7 @@ function TeamsSection() {
|
||||
setOpenMenuId(null);
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[loadTeams],
|
||||
[loadTeams, t.settings.teams.deleteConfirm, t.settings.teams.cannotDeleteDefault],
|
||||
);
|
||||
|
||||
const handleExpandTeam = useCallback(
|
||||
@@ -2432,7 +2463,7 @@ function TeamsSection() {
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
}
|
||||
},
|
||||
[quotaMb, retention, loadTeams],
|
||||
[quotaMb, retention, loadTeams, t.settings.teams.quotaSaved],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -2787,7 +2818,14 @@ function RolesSection() {
|
||||
}
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[newName, newDescription, newPermissions, loadRoles],
|
||||
[
|
||||
newName,
|
||||
newDescription,
|
||||
newPermissions,
|
||||
loadRoles,
|
||||
t.settings.roles.duplicateRoleError,
|
||||
t.settings.roles.createSuccess,
|
||||
],
|
||||
);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
@@ -2809,7 +2847,14 @@ function RolesSection() {
|
||||
}
|
||||
setTimeout(() => setActionMsg(null), 3000);
|
||||
},
|
||||
[editingRole, editName, editDescription, editPermissions, loadRoles],
|
||||
[
|
||||
editingRole,
|
||||
editName,
|
||||
editDescription,
|
||||
editPermissions,
|
||||
loadRoles,
|
||||
t.settings.roles.updateSuccess,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@@ -3213,8 +3258,11 @@ function AuditLogSection() {
|
||||
<div className="divide-y divide-border">
|
||||
{entries.map((entry) => (
|
||||
<Fragment key={entry.id}>
|
||||
<div
|
||||
className="px-3 py-2.5 hover:bg-muted/20 cursor-pointer transition-colors"
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expandedId === entry.id}
|
||||
aria-controls={`audit-details-${entry.id}`}
|
||||
className="w-full px-3 py-2.5 hover:bg-muted/20 cursor-pointer transition-colors text-start"
|
||||
onClick={() => setExpandedId(expandedId === entry.id ? null : entry.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@@ -3239,9 +3287,9 @@ function AuditLogSection() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{expandedId === entry.id && entry.details && (
|
||||
<div className="px-3 py-2 bg-muted/10">
|
||||
<div id={`audit-details-${entry.id}`} className="px-3 py-2 bg-muted/10">
|
||||
<pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono overflow-x-auto">
|
||||
{JSON.stringify(entry.details, null, 2)}
|
||||
</pre>
|
||||
|
||||
@@ -42,14 +42,15 @@ export function DocumentView({ inputOnly = false }: { inputOnly?: boolean } = {}
|
||||
// F22: when processedUrl is available, use URL-based loading instead of
|
||||
// entry.file (which is the original non-PDF input and would fail pdf.js)
|
||||
const file = hasProcessedUrl ? undefined : entry?.file;
|
||||
if (!file && !src) return;
|
||||
const url = src;
|
||||
if (!file && !url) return;
|
||||
let cancelled = false;
|
||||
let doc: pdfjs.PDFDocumentProxy | undefined;
|
||||
let renderTask: pdfjs.RenderTask | undefined;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url: src! };
|
||||
const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url };
|
||||
doc = await pdfjs.getDocument(source).promise;
|
||||
if (cancelled) return;
|
||||
setPageCount(doc.numPages);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight, Crown, Search } from "lucide-react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { formatFileSize } from "@/lib/download";
|
||||
import type { DuplicateFileInfo } from "@/stores/duplicate-store";
|
||||
import { useDuplicateStore } from "@/stores/duplicate-store";
|
||||
@@ -251,7 +250,6 @@ function DetailComparison() {
|
||||
}
|
||||
|
||||
export function FindDuplicatesResults() {
|
||||
const { t } = useTranslation();
|
||||
const { results, scanning, viewMode } = useDuplicateStore();
|
||||
|
||||
if (scanning) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Download, FolderArchive, Loader2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { formatFileSize } from "@/lib/download";
|
||||
import type { DuplicateResult } from "@/stores/duplicate-store";
|
||||
@@ -16,7 +15,6 @@ const PRESET_DESCRIPTIONS: Record<Preset, string> = {
|
||||
};
|
||||
|
||||
export function FindDuplicatesSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { files } = useFileStore();
|
||||
const {
|
||||
results,
|
||||
@@ -217,7 +215,7 @@ export function FindDuplicatesSettings() {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [files, results]);
|
||||
|
||||
const handleDownloadAll = useCallback(async () => {
|
||||
const _handleDownloadAll = useCallback(async () => {
|
||||
const { zipSync } = await import("fflate");
|
||||
|
||||
const zipData: Record<string, Uint8Array> = {};
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface GifToolsControlsProps {
|
||||
}
|
||||
|
||||
export function GifToolsControls({ settings: initialSettings, onChange }: GifToolsControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { info, loading: infoLoading } = useGifInfo();
|
||||
const isAnimated = (info?.pages ?? 0) > 1;
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ export function HtmlToImageResults() {
|
||||
);
|
||||
}
|
||||
|
||||
const resultUrl = store.resultUrl;
|
||||
|
||||
const handleDownload = () => {
|
||||
const a = document.createElement("a");
|
||||
a.href = store.resultUrl!;
|
||||
a.href = resultUrl;
|
||||
a.download = `screenshot.${store.format}`;
|
||||
a.click();
|
||||
};
|
||||
@@ -35,7 +37,7 @@ export function HtmlToImageResults() {
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<img
|
||||
src={store.resultUrl}
|
||||
src={resultUrl}
|
||||
alt="Captured screenshot"
|
||||
className="mx-auto max-w-full rounded-lg border border-border shadow-sm"
|
||||
/>
|
||||
@@ -49,6 +51,7 @@ export function HtmlToImageResults() {
|
||||
: ""}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
|
||||
>
|
||||
|
||||
@@ -45,8 +45,11 @@ export function HtmlToImageSettings() {
|
||||
|
||||
{store.mode === "url" && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.url}</label>
|
||||
<label htmlFor="html-to-image-url" className="mb-1 block text-sm font-medium">
|
||||
{ts.url}
|
||||
</label>
|
||||
<input
|
||||
id="html-to-image-url"
|
||||
type="url"
|
||||
value={store.url}
|
||||
onChange={(e) => store.setUrl(e.target.value)}
|
||||
@@ -58,8 +61,11 @@ export function HtmlToImageSettings() {
|
||||
|
||||
{store.mode === "html" && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.htmlFile}</label>
|
||||
<label htmlFor="html-to-image-file" className="mb-1 block text-sm font-medium">
|
||||
{ts.htmlFile}
|
||||
</label>
|
||||
<input
|
||||
id="html-to-image-file"
|
||||
type="file"
|
||||
accept=".html,.htm"
|
||||
onChange={(e) => {
|
||||
@@ -74,8 +80,11 @@ export function HtmlToImageSettings() {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.format}</label>
|
||||
<label htmlFor="html-to-image-format" className="mb-1 block text-sm font-medium">
|
||||
{ts.format}
|
||||
</label>
|
||||
<select
|
||||
id="html-to-image-format"
|
||||
value={store.format}
|
||||
onChange={(e) => store.setFormat(e.target.value as "jpg" | "png" | "webp")}
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||
@@ -88,10 +97,11 @@ export function HtmlToImageSettings() {
|
||||
|
||||
{store.format !== "png" && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
<label htmlFor="html-to-image-quality" className="mb-1 block text-sm font-medium">
|
||||
{ts.quality}: {store.quality}%
|
||||
</label>
|
||||
<input
|
||||
id="html-to-image-quality"
|
||||
type="range"
|
||||
min={1}
|
||||
max={100}
|
||||
@@ -103,8 +113,11 @@ export function HtmlToImageSettings() {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.devicePreset}</label>
|
||||
<label htmlFor="html-to-image-device-preset" className="mb-1 block text-sm font-medium">
|
||||
{ts.devicePreset}
|
||||
</label>
|
||||
<select
|
||||
id="html-to-image-device-preset"
|
||||
value={store.devicePreset}
|
||||
onChange={(e) =>
|
||||
store.setDevicePreset(e.target.value as "desktop" | "tablet" | "mobile" | "custom")
|
||||
@@ -121,8 +134,14 @@ export function HtmlToImageSettings() {
|
||||
{store.devicePreset === "custom" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.viewportWidth}</label>
|
||||
<label
|
||||
htmlFor="html-to-image-viewport-width"
|
||||
className="mb-1 block text-sm font-medium"
|
||||
>
|
||||
{ts.viewportWidth}
|
||||
</label>
|
||||
<input
|
||||
id="html-to-image-viewport-width"
|
||||
type="number"
|
||||
min={320}
|
||||
max={3840}
|
||||
@@ -132,8 +151,14 @@ export function HtmlToImageSettings() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">{ts.viewportHeight}</label>
|
||||
<label
|
||||
htmlFor="html-to-image-viewport-height"
|
||||
className="mb-1 block text-sm font-medium"
|
||||
>
|
||||
{ts.viewportHeight}
|
||||
</label>
|
||||
<input
|
||||
id="html-to-image-viewport-height"
|
||||
type="number"
|
||||
min={320}
|
||||
max={2160}
|
||||
@@ -146,8 +171,11 @@ export function HtmlToImageSettings() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">{ts.fullPage}</label>
|
||||
<label htmlFor="html-to-image-full-page" className="text-sm font-medium">
|
||||
{ts.fullPage}
|
||||
</label>
|
||||
<button
|
||||
id="html-to-image-full-page"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={store.fullPage}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useEffect, useRef, 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 EnhancementMode = "auto" | "portrait" | "landscape" | "low-light" | "food" | "document";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Check, ClipboardCopy, Download, FileJson, FileText, Loader2 } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import type { Base64Result } from "@/stores/base64-store";
|
||||
import { useBase64Store } from "@/stores/base64-store";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
@@ -169,7 +168,6 @@ function FileResult({ result }: { result: Base64Result }) {
|
||||
// -- Main ResultsPanel ------------------------------------------------------
|
||||
|
||||
export function ImageToBase64Results() {
|
||||
const { t } = useTranslation();
|
||||
const { results, errors, processing, progress } = useBase64Store();
|
||||
const { entries, selectedIndex, originalBlobUrl, selectedFileName } = useFileStore();
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { format } from "@/lib/format";
|
||||
import { useBase64Store } from "@/stores/base64-store";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
@@ -16,7 +14,6 @@ const OUTPUT_FORMATS = [
|
||||
] as const;
|
||||
|
||||
export function ImageToBase64Settings() {
|
||||
const { t } = useTranslation();
|
||||
const { files } = useFileStore();
|
||||
const { processing, setProcessing, setProgress, addResult, addError, reset } = useBase64Store();
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export function InfoSettings() {
|
||||
cacheRef.current.clear();
|
||||
autoFetchRef.current = false;
|
||||
setInfo(null);
|
||||
}, [files.length]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoFetchRef.current || files.length === 0) return;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
FONT_OPTIONS,
|
||||
@@ -328,7 +327,6 @@ function ResultSettings() {
|
||||
// ── Main Settings Component ─────────────────────────────────────────
|
||||
|
||||
export function MemeGeneratorSettings() {
|
||||
const { t } = useTranslation();
|
||||
const phase = useMemeStore((s) => s.phase);
|
||||
|
||||
if (phase === "gallery") return <GallerySettings />;
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { create } from "zustand";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
@@ -337,7 +336,6 @@ function CountryOption({
|
||||
// ── Settings panel (left side) ─────────────────────────────────────
|
||||
|
||||
export function PassportPhotoSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { files } = useFileStore();
|
||||
const { error } = useToolProcessor("passport-photo");
|
||||
|
||||
@@ -896,7 +894,6 @@ export function PassportPhotoSettings() {
|
||||
// ── Preview panel (right side) ────────────────────────────────────
|
||||
|
||||
export function PassportPhotoPreview() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
analyzeResult,
|
||||
countryCode,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import QRCodeStyling from "qr-code-styling";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { CollapsibleSection } from "@/components/common/collapsible-section";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import {
|
||||
type ContentType,
|
||||
type CornerDotType,
|
||||
@@ -346,7 +345,6 @@ function PillButton({
|
||||
// ── Main settings component ──────────────────────────────────────────
|
||||
|
||||
export function QrGenerateSettings() {
|
||||
const { t } = useTranslation();
|
||||
const store = useQrStore();
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export function ResizeControls({ settings: initialSettings, onChange }: ResizeCo
|
||||
blurRadius,
|
||||
sobelThreshold,
|
||||
squareMode,
|
||||
contentAware,
|
||||
]);
|
||||
|
||||
const handlePreset = (preset: (typeof SOCIAL_MEDIA_PRESETS)[number]) => {
|
||||
|
||||
@@ -15,7 +15,6 @@ export function RestorePhotoControls({
|
||||
settings: initialSettings,
|
||||
onChange,
|
||||
}: RestorePhotoControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [scratchRemoval, setScratchRemoval] = useState(true);
|
||||
const [faceEnhancement, setFaceEnhancement] = useState(true);
|
||||
const [fidelity, setFidelity] = useState(70);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useRef, 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 CropMode = "subject" | "face" | "trim";
|
||||
@@ -551,7 +550,6 @@ export function SmartCropControls({ settings: initialSettings, onChange }: Smart
|
||||
}
|
||||
|
||||
export function SmartCropSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processAllFiles, processing, error, progress } =
|
||||
useToolProcessor("smart-crop");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Download, Loader2, PackageOpen } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { CollapsibleSection } from "@/components/common/collapsible-section";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
import type { SplitMode } from "@/stores/split-store";
|
||||
@@ -36,7 +35,6 @@ const OUTPUT_FORMATS = [
|
||||
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif", "jxl"]);
|
||||
|
||||
export function SplitSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { files, processing: fileStoreProcessing } = useFileStore();
|
||||
const {
|
||||
mode,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Download, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
@@ -10,7 +9,6 @@ type Alignment = "start" | "center" | "end";
|
||||
type OutputFormat = "png" | "jpeg" | "webp" | "avif" | "jxl";
|
||||
|
||||
export function StitchSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
|
||||
useFileStore();
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export function TransparencyFixerControls({
|
||||
settings: _settings,
|
||||
onChange,
|
||||
}: TransparencyFixerControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [defringe, setDefringe] = useState(30);
|
||||
const [outputFormat, setOutputFormat] = useState<OutputFormat>("png");
|
||||
const [removeWatermark, setRemoveWatermark] = useState(false);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useEffect, useRef, 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 Position = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "tiled";
|
||||
|
||||
@@ -67,7 +67,7 @@ export function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>,
|
||||
return () => {
|
||||
container.removeEventListener("keydown", handleKeyDown);
|
||||
observer.disconnect();
|
||||
if (returnFocusRef.current && returnFocusRef.current.isConnected) {
|
||||
if (returnFocusRef.current?.isConnected) {
|
||||
returnFocusRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -363,7 +363,14 @@ export function AutomatePage() {
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}, [setSavedPipelines]);
|
||||
}, [
|
||||
setSavedPipelines,
|
||||
t.automate.invalidPipelineFile,
|
||||
t.automate.noSteps,
|
||||
t.automate.newerVersion,
|
||||
t.automate.missingName,
|
||||
t.automate.couldNotRead,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!importError) return;
|
||||
@@ -418,13 +425,16 @@ export function AutomatePage() {
|
||||
*/
|
||||
function renderPipelinePreview(mode: "result" | "original") {
|
||||
const kind = currentEntry?.previewKind ?? "image";
|
||||
const sourceUrl = originalBlobUrl;
|
||||
if (!sourceUrl) return null;
|
||||
|
||||
if (mode === "result") {
|
||||
if (!processedUrl) return null;
|
||||
if (kind === "image") {
|
||||
return (
|
||||
<BeforeAfterSlider
|
||||
beforeSrc={originalBlobUrl!}
|
||||
afterSrc={processedUrl as string}
|
||||
beforeSrc={sourceUrl}
|
||||
afterSrc={processedUrl}
|
||||
beforeSize={originalSize ?? undefined}
|
||||
afterSize={processedSize ?? undefined}
|
||||
/>
|
||||
@@ -440,7 +450,7 @@ export function AutomatePage() {
|
||||
if (kind === "audio") {
|
||||
return (
|
||||
<Suspense fallback={<div className="text-sm text-muted-foreground">Loading...</div>}>
|
||||
<WaveformPlayer src={processedUrl as string} />
|
||||
<WaveformPlayer src={processedUrl} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -467,7 +477,7 @@ export function AutomatePage() {
|
||||
if (kind === "image") {
|
||||
return (
|
||||
<ImageViewer
|
||||
src={originalBlobUrl!}
|
||||
src={sourceUrl}
|
||||
filename={selectedFileName ?? files[0].name}
|
||||
fileSize={selectedFileSize ?? files[0].size}
|
||||
/>
|
||||
@@ -483,7 +493,7 @@ export function AutomatePage() {
|
||||
if (kind === "audio") {
|
||||
return (
|
||||
<Suspense fallback={<div className="text-sm text-muted-foreground">Loading...</div>}>
|
||||
<WaveformPlayer src={originalBlobUrl!} />
|
||||
<WaveformPlayer src={sourceUrl} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export function LoginPage() {
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const _data = await res.json().catch(() => ({}));
|
||||
setError(t.auth.invalidCredentials);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ function FileSelectionInfo({
|
||||
const entry = fileEntries[i];
|
||||
return (
|
||||
<button
|
||||
key={`${file.name}-${i}`}
|
||||
key={entry?.blobUrl ?? `${file.name}-${file.size}-${file.lastModified}`}
|
||||
type="button"
|
||||
onClick={() => onSelect(i)}
|
||||
className={`w-full flex items-center gap-1.5 text-xs rounded px-2 py-1.5 text-start transition-colors ${isSelected ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-muted"}`}
|
||||
|
||||
Reference in New Issue
Block a user