mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: frontend unlimited — raise all client-side caps and timeouts
- XHR timeouts: doubled across all tool categories (120s/300s/600s) - Pipeline timeout: 180s → 600s - Favicon/PDF/barcode timeouts raised to 300s - Files page: fetch 200 per page - Split grid: 20 → 100 max columns/rows - Image viewer zoom: 25-300% → 10-1000% - Collage zoom: 3x → 10x, pan: ±100 → ±200 - Passport photo: face adjust ±15% → ±30%, zoom 3x → 5x
This commit is contained in:
@@ -27,7 +27,7 @@ interface ImageViewerProps {
|
|||||||
imageWrapperStyle?: React.CSSProperties;
|
imageWrapperStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 200, 300];
|
const ZOOM_STEPS = [10, 25, 50, 75, 100, 150, 200, 300, 500, 1000];
|
||||||
const DEFAULT_ZOOM = 100;
|
const DEFAULT_ZOOM = 100;
|
||||||
|
|
||||||
export function ImageViewer({
|
export function ImageViewer({
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function scanOneFile(
|
|||||||
formData.append("settings", JSON.stringify({ tryHarder }));
|
formData.append("settings", JSON.stringify({ tryHarder }));
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.timeout = 60_000;
|
xhr.timeout = 300_000;
|
||||||
|
|
||||||
xhr.upload.onprogress = (e) => {
|
xhr.upload.onprogress = (e) => {
|
||||||
if (e.lengthComputable) onUploadProgress((e.loaded / e.total) * 100);
|
if (e.lengthComputable) onUploadProgress((e.loaded / e.total) * 100);
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ function CollageCell({
|
|||||||
if (first) memo = { panX: transform.panX, panY: transform.panY };
|
if (first) memo = { panX: transform.panX, panY: transform.panY };
|
||||||
const rect = cellRef.current?.getBoundingClientRect();
|
const rect = cellRef.current?.getBoundingClientRect();
|
||||||
if (!rect || !memo) return memo;
|
if (!rect || !memo) return memo;
|
||||||
const panX = Math.max(-100, Math.min(100, memo.panX + (mx / rect.width) * 100));
|
const panX = Math.max(-200, Math.min(200, memo.panX + (mx / rect.width) * 100));
|
||||||
const panY = Math.max(-100, Math.min(100, memo.panY + (my / rect.height) * 100));
|
const panY = Math.max(-200, Math.min(200, memo.panY + (my / rect.height) * 100));
|
||||||
store.setCellTransform(cellIndex, { panX, panY });
|
store.setCellTransform(cellIndex, { panX, panY });
|
||||||
return memo;
|
return memo;
|
||||||
},
|
},
|
||||||
@@ -360,11 +360,11 @@ function CollageCell({
|
|||||||
const bindPinch = usePinch(
|
const bindPinch = usePinch(
|
||||||
({ offset: [scale] }) => {
|
({ offset: [scale] }) => {
|
||||||
if (!image || !isSelected) return;
|
if (!image || !isSelected) return;
|
||||||
const zoom = Math.max(1, Math.min(3, scale));
|
const zoom = Math.max(1, Math.min(10, scale));
|
||||||
store.setCellTransform(cellIndex, { zoom });
|
store.setCellTransform(cellIndex, { zoom });
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scaleBounds: { min: 1, max: 3 },
|
scaleBounds: { min: 1, max: 10 },
|
||||||
from: () => [transform.zoom, 0],
|
from: () => [transform.zoom, 0],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -378,7 +378,7 @@ function CollageCell({
|
|||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||||
const zoom = Math.max(1, Math.min(3, zoomRef.current + delta));
|
const zoom = Math.max(1, Math.min(10, zoomRef.current + delta));
|
||||||
store.setCellTransform(cellIndex, { zoom });
|
store.setCellTransform(cellIndex, { zoom });
|
||||||
};
|
};
|
||||||
el.addEventListener("wheel", handleWheel, { passive: false });
|
el.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
@@ -555,7 +555,7 @@ function CollageCell({
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="3"
|
max="10"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={transform.zoom}
|
value={transform.zoom}
|
||||||
onChange={handleZoomSlider}
|
onChange={handleZoomSlider}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function FaviconSettings() {
|
|||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhrRef.current = xhr;
|
xhrRef.current = xhr;
|
||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
xhr.timeout = 180_000;
|
xhr.timeout = 300_000;
|
||||||
|
|
||||||
xhr.upload.onprogress = (event) => {
|
xhr.upload.onprogress = (event) => {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ImageToPdfSettings() {
|
|||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhrRef.current = xhr;
|
xhrRef.current = xhr;
|
||||||
xhr.timeout = 180_000;
|
xhr.timeout = 300_000;
|
||||||
|
|
||||||
xhr.upload.onprogress = (event) => {
|
xhr.upload.onprogress = (event) => {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
|
|||||||
@@ -1062,8 +1062,8 @@ export function PassportPhotoPreview() {
|
|||||||
if (!dragStartRef.current) return;
|
if (!dragStartRef.current) return;
|
||||||
const dx = (e.clientX - dragStartRef.current.x) * 0.001;
|
const dx = (e.clientX - dragStartRef.current.x) * 0.001;
|
||||||
const dy = (e.clientY - dragStartRef.current.y) * 0.001;
|
const dy = (e.clientY - dragStartRef.current.y) * 0.001;
|
||||||
setAdjustX(Math.max(-0.15, Math.min(0.15, dragStartRef.current.ax - dx)));
|
setAdjustX(Math.max(-0.3, Math.min(0.3, dragStartRef.current.ax - dx)));
|
||||||
setAdjustY(Math.max(-0.15, Math.min(0.15, dragStartRef.current.ay - dy)));
|
setAdjustY(Math.max(-0.3, Math.min(0.3, dragStartRef.current.ay - dy)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseUp() {
|
function handleMouseUp() {
|
||||||
@@ -1083,7 +1083,7 @@ export function PassportPhotoPreview() {
|
|||||||
const handleWheel = useCallback(
|
const handleWheel = useCallback(
|
||||||
(e: React.WheelEvent<HTMLCanvasElement>) => {
|
(e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setZoom(Math.max(0.5, Math.min(3, zoom + (e.deltaY > 0 ? -0.1 : 0.1))));
|
setZoom(Math.max(0.5, Math.min(5, zoom + (e.deltaY > 0 ? -0.1 : 0.1))));
|
||||||
},
|
},
|
||||||
[zoom, setZoom],
|
[zoom, setZoom],
|
||||||
);
|
);
|
||||||
@@ -1138,7 +1138,7 @@ export function PassportPhotoPreview() {
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setZoom(Math.min(3, zoom + 0.25))}
|
onClick={() => setZoom(Math.min(5, zoom + 0.25))}
|
||||||
className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||||
title="Zoom in"
|
title="Zoom in"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ export function usePipelineProcessor() {
|
|||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhrRef.current = xhr;
|
xhrRef.current = xhr;
|
||||||
|
|
||||||
// Pipeline runs multiple steps sequentially, allow up to 3 minutes
|
// Pipeline runs multiple steps sequentially, allow up to 10 minutes
|
||||||
xhr.timeout = 180_000;
|
xhr.timeout = 600_000;
|
||||||
|
|
||||||
// Pipeline is always "medium" speed: upload = 0-40%, processing = 40-95%
|
// Pipeline is always "medium" speed: upload = 0-40%, processing = 40-95%
|
||||||
const UPLOAD_WEIGHT = 40;
|
const UPLOAD_WEIGHT = 40;
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ export function useToolProcessor(toolId: string) {
|
|||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhrRef.current = xhr;
|
xhrRef.current = xhr;
|
||||||
|
|
||||||
// Timeout: 60s for fast tools, 3 min for medium (seam carving), 5 min for AI
|
// Timeout: 2 min for fast tools, 5 min for medium (seam carving), 10 min for AI
|
||||||
xhr.timeout = isAiTool ? 300_000 : isMediumTool ? 180_000 : 60_000;
|
xhr.timeout = isAiTool ? 600_000 : isMediumTool ? 300_000 : 120_000;
|
||||||
|
|
||||||
// For AI tools: upload = 0-15%, processing = 15-100% (SSE-driven)
|
// For AI tools: upload = 0-15%, processing = 15-100% (SSE-driven)
|
||||||
// For medium tools: upload = 0-40%, processing = 40-95% (gradual fill)
|
// For medium tools: upload = 0-40%, processing = 40-95% (gradual fill)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const useFilesPageStore = create<FilesPageState>((set, get) => ({
|
|||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const { searchQuery } = get();
|
const { searchQuery } = get();
|
||||||
const result = await apiListFiles({ search: searchQuery || undefined, limit: 100 });
|
const result = await apiListFiles({ search: searchQuery || undefined, limit: 200 });
|
||||||
set({ files: result.files, total: result.total, loading: false });
|
set({ files: result.files, total: result.total, loading: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
set({ error: err instanceof Error ? err.message : "Failed to load files", loading: false });
|
set({ error: err instanceof Error ? err.message : "Failed to load files", loading: false });
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ export const useSplitStore = create<SplitState>((set, get) => ({
|
|||||||
|
|
||||||
setMode: (mode) => set({ mode, tiles: [], zipBlobUrl: null, error: null }),
|
setMode: (mode) => set({ mode, tiles: [], zipBlobUrl: null, error: null }),
|
||||||
setColumns: (columns) =>
|
setColumns: (columns) =>
|
||||||
set({ columns: Math.max(1, Math.min(20, columns)), tiles: [], zipBlobUrl: null }),
|
set({ columns: Math.max(1, Math.min(100, columns)), tiles: [], zipBlobUrl: null }),
|
||||||
setRows: (rows) => set({ rows: Math.max(1, Math.min(20, rows)), tiles: [], zipBlobUrl: null }),
|
setRows: (rows) => set({ rows: Math.max(1, Math.min(100, rows)), tiles: [], zipBlobUrl: null }),
|
||||||
setTileWidth: (tileWidth) =>
|
setTileWidth: (tileWidth) =>
|
||||||
set({ tileWidth: Math.max(10, tileWidth), tiles: [], zipBlobUrl: null }),
|
set({ tileWidth: Math.max(10, tileWidth), tiles: [], zipBlobUrl: null }),
|
||||||
setTileHeight: (tileHeight) =>
|
setTileHeight: (tileHeight) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user