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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user