mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Fix mobile scanner upload flow and fit it to one screen (#7684)
file mobile phone scanner UI issues when on http and scaling UI issues Ensuring that smaller screens dont cut off UI elements better handling of batch photos <img width="2104" height="8800" alt="montage_mobile-scanner" src="https://github.com/user-attachments/assets/b4dd114b-c54d-4101-8700-7307dbb0eee9" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
This commit is contained in:
@@ -5416,39 +5416,45 @@ sort = "Sort"
|
||||
title = "Merge Settings Overview"
|
||||
|
||||
[mobileScanner]
|
||||
addToBatch = "Add to Batch"
|
||||
addMore = "Add More"
|
||||
back = "Back"
|
||||
batchImages = "Batch"
|
||||
camera = "Camera"
|
||||
cameraAccessDenied = "Camera access denied. Please enable camera access."
|
||||
cameraDescription = "Scan documents using your device camera with automatic edge detection"
|
||||
capture = "Capture Photo"
|
||||
chooseMethod = "Choose Upload Method"
|
||||
chooseMethodDescription = "Select how you want to scan and upload documents"
|
||||
clearBatch = "Clear"
|
||||
clearAll = "Clear All"
|
||||
closeTabHint = "You can close this tab now."
|
||||
dismiss = "Dismiss"
|
||||
edgeDetection = "Edge Detection"
|
||||
fileDescription = "Upload existing photos or documents from your device"
|
||||
fileReadFailed = "Could not read that file."
|
||||
fileUpload = "File Upload"
|
||||
flash = "Flash"
|
||||
flashlight = "Flashlight"
|
||||
httpsRequired = "Camera access requires HTTPS or localhost. Please use HTTPS or access via localhost."
|
||||
noSession = "Invalid Session"
|
||||
imageCount_one = "{{count}} image"
|
||||
imageCount_other = "{{count}} images"
|
||||
imagePosition = "Image {{index}} of {{total}}"
|
||||
invalidFileType = "Please choose an image file."
|
||||
noSessionMessage = "Please scan a valid QR code to access this page."
|
||||
processing = "Processing..."
|
||||
remove = "Remove"
|
||||
retake = "Retake"
|
||||
scanAnother = "Scan another"
|
||||
selectFilesPrompt = "Select files to upload"
|
||||
selectImage = "Select Image"
|
||||
selectImages = "Select Images"
|
||||
sessionExpired = "This session has expired. Please refresh and try again."
|
||||
sessionInvalid = "Session Error"
|
||||
sessionNotFound = "Session not found. Please refresh and try again."
|
||||
sessionValidationError = "Unable to verify session. Please try again."
|
||||
startingCamera = "Starting camera…"
|
||||
title = "Mobile Scanner"
|
||||
upload = "Upload"
|
||||
uploadAll = "Upload All"
|
||||
uploadFailed = "Upload failed. Please try again."
|
||||
uploading = "Uploading..."
|
||||
uploadSuccess = "Upload Successful!"
|
||||
uploadSuccessMessage = "Your images have been transferred."
|
||||
uploadWithCount = "Upload ({{total}})"
|
||||
validating = "Validating session..."
|
||||
|
||||
[mobileSign]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,8 +100,11 @@ export async function handleHttpError(error: unknown): Promise<boolean> {
|
||||
pathname.includes("/auth/") ||
|
||||
pathname.includes("/invite/");
|
||||
|
||||
const isPublicMobilePage =
|
||||
pathname.includes("/mobile-scanner") || pathname.includes("/mobile-sign");
|
||||
|
||||
// If not on auth page, redirect to login with expired session message
|
||||
if (!isAuthPage && !skipAuthRedirect) {
|
||||
if (!isAuthPage && !isPublicMobilePage && !skipAuthRedirect) {
|
||||
if (loginRedirectRecentlyFired()) {
|
||||
console.warn(
|
||||
"[httpErrorHandler] 401 redirect already fired moments ago — suppressing repeat to avoid a login loop:",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Unit tests for fitThumbs, which sizes the mobile scanner's thumbnail strip.
|
||||
*
|
||||
* The page deliberately never scrolls, so the strip shrinks to fit instead.
|
||||
* These pin the invariants that keep it from swallowing the camera preview.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { fitThumbs, THUMB_SIZES } from "@app/utils/mobileScannerThumbs";
|
||||
|
||||
const TOTALS = [1, 3, 9, 21, 40];
|
||||
const VIEWPORTS = [
|
||||
{ width: 240, height: 320 },
|
||||
{ width: 1024, height: 1366 },
|
||||
];
|
||||
|
||||
describe("fitThumbs", () => {
|
||||
test("a zero viewport falls back to the largest thumb and no height cap", () => {
|
||||
expect(fitThumbs(3, 0, 0)).toEqual({
|
||||
thumbSize: THUMB_SIZES[0],
|
||||
stripMaxHeight: undefined,
|
||||
});
|
||||
expect(fitThumbs(3, 375, 0)).toEqual({
|
||||
thumbSize: THUMB_SIZES[0],
|
||||
stripMaxHeight: undefined,
|
||||
});
|
||||
expect(fitThumbs(3, 0, 812)).toEqual({
|
||||
thumbSize: THUMB_SIZES[0],
|
||||
stripMaxHeight: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("stripMaxHeight never exceeds 45% of the viewport height", () => {
|
||||
for (const { width, height } of VIEWPORTS) {
|
||||
for (const total of TOTALS) {
|
||||
const { stripMaxHeight } = fitThumbs(total, width, height);
|
||||
expect(stripMaxHeight).toBeLessThanOrEqual(height * 0.45);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("thumbSize never grows as more images are added", () => {
|
||||
for (const { width, height } of VIEWPORTS) {
|
||||
let previous = Number.POSITIVE_INFINITY;
|
||||
for (let total = 1; total <= 60; total++) {
|
||||
const { thumbSize } = fitThumbs(total, width, height);
|
||||
expect(thumbSize).toBeLessThanOrEqual(previous);
|
||||
previous = thumbSize;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("thumbSize is always one of the allowed sizes", () => {
|
||||
for (const { width, height } of VIEWPORTS) {
|
||||
for (const total of TOTALS) {
|
||||
expect(THUMB_SIZES).toContain(
|
||||
fitThumbs(total, width, height).thumbSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
export const THUMB_SIZES = [52, 44, 38, 32, 26, 20];
|
||||
export const THUMB_GAP = 4;
|
||||
|
||||
/**
|
||||
* Pick the largest thumbnail size whose wrapped strip still fits the share of
|
||||
* the viewport the mobile scanner reserves for it, and the height cap to render
|
||||
* that strip at. Keeps the page to one screen with no scrolling at any size.
|
||||
*/
|
||||
export function fitThumbs(
|
||||
total: number,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
) {
|
||||
if (!viewportWidth || !viewportHeight) {
|
||||
return { thumbSize: THUMB_SIZES[0], stripMaxHeight: undefined };
|
||||
}
|
||||
const rowWidth = Math.max(viewportWidth - 24, 80);
|
||||
const heightFor = (size: number) => {
|
||||
const perRow = Math.max(
|
||||
1,
|
||||
Math.floor((rowWidth + THUMB_GAP) / (size + THUMB_GAP)),
|
||||
);
|
||||
return Math.ceil(total / perRow) * (size + THUMB_GAP);
|
||||
};
|
||||
const preferred = viewportHeight * 0.24;
|
||||
const thumbSize =
|
||||
THUMB_SIZES.find((size) => heightFor(size) <= preferred) ??
|
||||
THUMB_SIZES[THUMB_SIZES.length - 1];
|
||||
// Let the strip grow past its usual share rather than hide images, but never
|
||||
// far enough to swallow the preview.
|
||||
const stripMaxHeight = Math.min(
|
||||
Math.max(preferred, heightFor(thumbSize)),
|
||||
viewportHeight * 0.45,
|
||||
);
|
||||
return { thumbSize, stripMaxHeight };
|
||||
}
|
||||
Reference in New Issue
Block a user