camers scan init

This commit is contained in:
Anthony Stirling 2025-06-04 15:45:50 +01:00
parent 488fe253aa
commit a187f0599b
26 changed files with 1963 additions and 9 deletions

View File

@ -13,7 +13,8 @@
"Bash(fi)",
"Bash(ls:*)",
"Bash(./testing/cucumber/features/environment.py -v)",
"Bash(./testing/cucumber/features/autojob.feature -v)"
"Bash(./testing/cucumber/features/autojob.feature -v)",
"Bash(git rev-parse:*)"
],
"deny": []
}

View File

@ -0,0 +1,56 @@
# Scan Upload Feature Improvements
## Code Improvements
1. **Modular JavaScript Architecture**
- Split the monolithic `scan-upload-direct.js` into logical modules:
- `logger.js`: Handles logging and status updates
- `peer-connection.js`: Manages WebRTC peer connections
- `camera.js`: Controls mobile camera functionality
- `scan-upload.js`: Main module that coordinates everything
2. **Separated CSS from HTML**
- Created dedicated CSS files:
- `scan-upload.css`: Styles for the desktop scan-upload page
- `mobile-scanner.css`: Styles for the mobile camera interface
3. **Improved Error Handling**
- Added better error handling throughout the codebase
- Improved user feedback for connection and camera issues
- Enhanced debug logging capabilities
4. **Backward Compatibility**
- Created a compatibility layer that maintains the old API
- Allows gradual migration to the new code structure
- Ensures existing integrations won't break
## UI Improvements
1. **Enhanced Responsive Design**
- Improved mobile layout with proper media queries
- Better handling of different screen sizes
2. **Better Visual Feedback**
- Clearer status messages for users
- Improved styling of the scan result display
- Enhanced debug information presentation
3. **Localization Support**
- Added proper Thymeleaf text references for all UI elements
- Uses the existing messages system for translations
## Security Improvements
1. **Enhanced WebRTC Implementation**
- Better handling of connection errors
- Improved security for peer connections
- Structured error handling for failed connections
## Next Steps
Potential future improvements:
1. Add more robust testing for the WebRTC functionality
2. Consider implementing a fallback method if WebRTC is not available
3. Add support for scanning multiple documents in one session
4. Implement better image quality control options

View File

@ -24,7 +24,9 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
"file",
"messageType",
"infoMessage",
"async");
"async",
"session",
"t");
@Override
public boolean preHandle(

View File

@ -173,6 +173,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("Other", "remove-image-pdf");
addEndpointToGroup("Other", "scan-upload");
// CLI
addEndpointToGroup("CLI", "compress-pdf");

View File

@ -0,0 +1,24 @@
package stirling.software.SPDF.controller.api.misc;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
/**
* Controller for scan upload functionality - WebRTC version.
*
* This controller is completely empty as all functionality has been moved to WebRTC.
* The image transfer happens directly between browsers without any server involvement.
*/
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@Hidden
@Slf4j
public class ScanUploadController {
// All functionality has been moved to client-side WebRTC
}

View File

@ -0,0 +1,49 @@
package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Web controller for the scan-upload functionality.
*/
@Controller
@Tag(name = "Scan Upload", description = "Scan Upload Web Interface")
@Slf4j
@RequiredArgsConstructor
public class ScanUploadWebController {
/**
* Serves the scan-upload page for desktop/PC view.
*
* @param model the Spring MVC model
* @return the template name to render
*/
@GetMapping("/scan-upload")
@Hidden
public String scanUploadForm(Model model) {
model.addAttribute("currentPage", "scan-upload");
return "misc/scan-upload";
}
/**
* Serves the mobile page for camera capture and upload.
*
* @param model the Spring MVC model
* @param session the session ID parameter
* @return the template name to render
*/
@GetMapping("/mobile")
@Hidden
public String mobileView(Model model, @RequestParam(name = "session", required = false) String session) {
model.addAttribute("sessionId", session);
return "misc/mobile";
}
}

View File

@ -1549,7 +1549,7 @@ validateSignature.cert.bits=bits
####################
cookieBanner.popUp.title=How we use Cookies
cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.
cookieBanner.popUp.description.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.description.2=If you'd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.acceptAllBtn=Okay
cookieBanner.popUp.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1565,7 +1565,7 @@ cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off.
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can't be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
@ -1604,3 +1604,30 @@ fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)
#scan-upload
scan-upload.title=Scan and Upload
scan-upload.description=Scan a document or photo with your phone and send it directly to this browser.
scan-upload.instructions.title=Scan this QR code with your phone
scan-upload.instructions.text=Use your phone's camera to scan the QR code, then take a photo of your document.
scan-upload.session=Session ID:
scan-upload.result=Scan Result
scan-upload.download=Download
scan-upload.new-scan=New Scan
#mobile
mobile.title=Mobile Scanner
mobile.align-document=Align your document within the frame
mobile.retake=Retake
mobile.upload=Upload
mobile.uploading=Uploading...
mobile.success=Success!
mobile.success-message=Your scan has been successfully uploaded
mobile.camera-access=Camera Access Required
mobile.camera-permission=Please allow access to your camera to scan documents
mobile.grant-access=Grant Access
#home.scan-upload
home.scan-upload.title=Scan and Upload
home.scan-upload.desc=Scan a document with your phone and send it to your browser
scan-upload.tags=scan,upload,photo,camera,mobile

View File

@ -0,0 +1,415 @@
/* Mobile Scanner CSS */
body {
background: var(--md-sys-color-background, #000);
color: var(--md-sys-color-on-background, #fff);
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh; /* Fix height to prevent scrolling */
position: fixed; /* Prevent body scrolling */
width: 100%;
}
.app-header {
padding: 10px;
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.5);
}
.input-method-tabs {
display: flex;
border-radius: 8px;
overflow: hidden;
background: rgba(var(--md-sys-color-on-background, 255, 255, 255), 0.1);
margin-bottom: 10px;
}
.input-tab {
flex: 1;
padding: 12px;
text-align: center;
background: transparent;
border: none;
color: var(--md-sys-color-on-background, #fff);
font-weight: 500;
transition: background 0.3s;
cursor: pointer;
}
.input-tab.active {
background: var(--md-sys-color-primary, #4CAF50);
color: var(--md-sys-color-on-primary, #fff);
}
.file-upload-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 70vh;
padding: 20px;
}
.file-upload-label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 180px;
height: 180px;
border: 2px dashed var(--md-sys-color-outline, rgba(var(--md-sys-color-on-background, 255, 255, 255), 0.5));
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.file-upload-label:hover {
border-color: var(--md-sys-color-primary, #4CAF50);
background: rgba(255,255,255,0.05);
}
.upload-icon {
font-size: 48px;
color: var(--md-sys-color-primary, #4CAF50);
margin-bottom: 10px;
}
.upload-text {
color: var(--md-sys-color-on-background, #fff);
font-size: 16px;
}
.file-preview-container {
width: 100%;
max-width: 300px;
margin-top: 20px;
}
.file-preview {
width: 100%;
border-radius: 8px;
margin-bottom: 15px;
}
.file-actions {
display: flex;
justify-content: space-between;
gap: 8px;
}
.container, .review-container {
display: flex;
flex-direction: column;
height: calc(100vh - 50px); /* Account for header */
box-sizing: border-box;
padding: 10px;
overflow: hidden; /* Prevent container scrolling */
}
.camera-container {
flex: 1;
position: relative;
background: var(--md-sys-color-surface-container, #222);
border-radius: 10px;
overflow: hidden;
max-height: 70vh;
margin-bottom: 15px;
}
#camera-view {
width: 100%;
height: 100%;
object-fit: cover;
}
.controls, .review-controls {
position: fixed;
bottom: 20px;
left: 10px;
right: 10px;
display: flex;
justify-content: space-between;
z-index: 5;
}
.capture-btn, .review-btn {
border: none;
cursor: pointer;
font-weight: bold;
}
.capture-btn {
width: 70px;
height: 70px;
border-radius: 50%;
background: white;
position: relative;
border: 3px solid rgba(255,255,255,0.5);
margin: auto;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
}
.capture-btn::after {
content: '';
position: absolute;
top: 5px;
left: 5px;
width: 60px;
height: 60px;
background: white;
border-radius: 50%;
}
.review-container {
display: none;
padding-bottom: 80px; /* Make room for batch preview */
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
}
.preview-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.preview-controls {
display: flex;
align-items: center;
gap: 10px;
}
.batch-btn {
background: var(--md-sys-color-secondary, #4285F4);
color: var(--md-sys-color-on-secondary, white);
border: none;
border-radius: 20px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.batch-counter {
font-size: 14px;
color: var(--md-sys-color-on-background, white);
opacity: 0.8;
}
.batch-preview {
position: fixed;
bottom: 80px; /* Positioned above the camera controls */
left: 0;
right: 0;
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.8);
padding: 10px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
z-index: 6; /* Below the controls (z-index: 10) */
}
.loading-message {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.7);
color: var(--md-sys-color-on-background, #fff);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
font-size: 18px;
}
.batch-preview.active {
max-height: 120px;
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.batch-actions {
display: flex;
gap: 8px;
}
.upload-batch-btn {
background: var(--md-sys-color-primary, #4CAF50);
color: var(--md-sys-color-on-primary, white);
border: none;
border-radius: 15px;
padding: 4px 12px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.batch-header h4 {
margin: 0;
font-size: 14px;
font-weight: normal;
}
.clear-btn {
background: var(--md-sys-color-error, #f44336);
color: var(--md-sys-color-on-error, white);
border: none;
border-radius: 15px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
}
.batch-thumbnails {
display: flex;
overflow-x: auto;
gap: 10px;
padding-bottom: 5px;
}
.batch-thumbnail {
position: relative;
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 5px;
overflow: hidden;
}
.batch-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-thumbnail {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.6);
color: var(--md-sys-color-on-error, white);
border: none;
border-radius: 50%;
font-size: 10px;
line-height: 1;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#capture-preview {
width: 100%;
object-fit: contain;
border-radius: 10px;
background: var(--md-sys-color-surface-container, #222);
max-height: 70vh;
flex: 1;
}
.retake-btn {
background: var(--md-sys-color-error, #f44336);
color: var(--md-sys-color-on-error, white);
border-radius: 25px;
padding: 12px 20px;
width: 45%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
text-transform: uppercase;
font-weight: 500;
border: none;
}
.upload-btn {
background: var(--md-sys-color-primary, #4CAF50);
color: var(--md-sys-color-on-primary, white);
border-radius: 25px;
padding: 12px 20px;
width: 45%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
text-transform: uppercase;
font-weight: 500;
border: none;
}
.spinner, .success-message, .camera-error {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 1000;
text-align: center;
}
.spinner {
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.7);
}
.spinner-border {
border: 0.25em solid var(--md-sys-color-on-background, white);
border-right-color: transparent;
border-radius: 50%;
width: 3rem;
height: 3rem;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.success-message {
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.7);
color: var(--md-sys-color-on-background, white);
}
.success-message i {
font-size: 60px;
color: var(--md-sys-color-primary, #4CAF50);
margin-bottom: 20px;
}
.camera-error {
background: rgba(var(--md-elevation-shadow-color-rgb, 0, 0, 0), 0.9);
color: var(--md-sys-color-on-background, white);
padding: 20px;
}
/* Add more polished styling */
.spinner-text {
margin-top: 15px;
font-size: 16px;
font-weight: 500;
}
.success-message h2 {
font-weight: 500;
margin-top: 0;
}
.success-message p {
font-size: 16px;
opacity: 0.9;
}

View File

@ -0,0 +1,119 @@
/* Scan Upload CSS */
.scan-container {
margin: 20px auto;
max-width: 800px;
}
.card-header h2 {
color: var(--md-sys-color-on-surface, #000);
}
.card-body {
color: var(--md-sys-color-on-surface, #000);
}
#qrcode-container, .scan-result-container {
margin: 20px;
text-align: center;
}
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.image-card {
border-radius: 8px;
overflow: hidden;
background: var(--md-sys-color-surface-container, #fff);
box-shadow: 0 2px 5px rgba(var(--md-elevation-shadow-color-rgb), 0.2);
transition: transform 0.2s ease;
}
.image-card:hover {
transform: translateY(-3px);
}
.gallery-image {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.image-actions {
padding: 10px;
display: flex;
justify-content: space-between;
}
.image-action-btn {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
}
.session-id, .status-msg {
margin-top: 10px;
font-family: monospace;
background: var(--md-sys-color-surface-container-low, #f8f9fa);
color: var(--md-sys-color-on-surface, #000);
padding: 8px;
border-radius: 4px;
display: inline-block;
}
.actions {
margin-top: 15px;
display: flex;
justify-content: center;
gap: 10px;
}
.scan-info {
background-color: var(--md-sys-color-surface-variant, #f0f0f0);
border-left: 4px solid var(--md-sys-color-primary, #0d6efd);
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
color: var(--md-sys-color-on-surface-variant, #000);
}
.alert-info {
color: var(--md-sys-color-on-surface-variant, #000);
}
/* Ensure download button has proper colors */
#download-btn {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-color: var(--md-sys-color-primary);
}
#new-scan-btn {
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
border-color: var(--md-sys-color-secondary);
}
@media (max-width: 768px) {
.scan-container {
margin: 10px;
}
#qrcode-container, .scan-result-container {
margin: 10px;
}
.actions {
flex-direction: column;
align-items: center;
}
.actions .btn {
width: 100%;
margin-bottom: 10px;
}
}

View File

@ -16,8 +16,10 @@ function setLanguageForDropdown(dropdownClass) {
function updateUrlWithLanguage(languageCode) {
const currentURL = new URL(window.location.href);
currentURL.searchParams.set('lang', languageCode);
window.location.href = currentURL.toString();
if (currentURL.searchParams.get('lang') !== languageCode) {
currentURL.searchParams.set('lang', languageCode);
window.location.href = currentURL.toString();
}
}
function handleDropdownItemClick(event) {
@ -32,15 +34,25 @@ function handleDropdownItemClick(event) {
}
function checkUserLanguage(defaultLocale) {
const currentLanguageInDOM = document.documentElement.getAttribute('data-language');
const currentURL = new URL(window.location.href);
const langParam = currentURL.searchParams.get('lang');
if (
!localStorage.getItem('languageCode') ||
document.documentElement.getAttribute('data-language') != defaultLocale
currentLanguageInDOM !== defaultLocale ||
langParam !== defaultLocale
) {
localStorage.setItem('languageCode', defaultLocale);
updateUrlWithLanguage(defaultLocale);
if (langParam !== defaultLocale) {
currentURL.searchParams.set('lang', defaultLocale);
window.location.href = currentURL.toString();
}
}
}
function initLanguageSettings() {
document.addEventListener('DOMContentLoaded', function () {
setLanguageForDropdown('.lang_dropdown-item');

View File

@ -0,0 +1,65 @@
/**
* Backward compatibility script for scan-upload-direct.js
* This loads the modular version
*/
// Add async script loading
function loadScript(src, async = true) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.async = async;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Load the modules asynchronously
async function loadModules() {
try {
console.info('Loading scan-upload modules via compatibility layer...');
// If using type="module" is not supported, load individual scripts
if (typeof window.initScanUploadPC !== 'function' &&
typeof window.initScanUploadMobile !== 'function') {
// Load the necessary scripts
await loadScript('/js/scan-upload/logger.js', true);
await loadScript('/js/scan-upload/peer-connection.js', true);
await loadScript('/js/scan-upload/camera.js', true);
await loadScript('/js/scan-upload/scan-upload.js', true);
console.info('Modules loaded via compatibility layer');
}
} catch (error) {
console.error('Error loading modules:', error);
alert('Failed to load scan-upload modules. Please try refreshing the page.');
}
}
// Load scripts on page load
document.addEventListener('DOMContentLoaded', loadModules);
// Forward function calls to the module implementations
window.initScanUploadPC = function() {
if (typeof window.initScanUploadPC === 'function') {
return window.initScanUploadPC();
} else {
console.error('ScanUpload module not loaded correctly');
}
};
window.initScanUploadMobile = function() {
if (typeof window.initScanUploadMobile === 'function') {
return window.initScanUploadMobile();
} else {
console.error('ScanUpload module not loaded correctly');
}
};
// Backward compatibility stubs
window.generateRandomId = function() { return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, () => (Math.random() * 16 | 0).toString(16)); };
window.updateStatus = function(message) { console.log(message); };
window.showError = function(message) { alert("Upload Error: " + message); };
window.logDebug = function(message) { console.log("LOG:", message); };

View File

@ -0,0 +1,496 @@
/**
* Camera handling module for the mobile scanner
*/
import Logger from './logger.js';
import PeerConnection from './peer-connection.js';
const Camera = {
stream: null,
capturedImageData: null,
capturedImages: [],
/**
* Initialize the camera and file upload on mobile device
*/
init: function() {
// Initialize camera
this.initCamera();
// Set up tab switching
this.setupTabs();
// Set up file upload handling
this.setupFileUpload();
},
/**
* Initialize the camera
*/
initCamera: function() {
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
.then(stream => {
this.stream = stream;
// Camera stream started
document.getElementById('camera-view').srcObject = stream;
this.setupCameraControls();
})
.catch((err) => {
console.error("Camera error:", err);
document.getElementById('camera-error').style.display = 'flex';
Logger.showError("Camera access denied or unavailable");
// Automatically switch to file upload if camera fails
this.switchToFileUpload();
});
},
/**
* Set up tab switching between camera and file upload
*/
setupTabs: function() {
const cameraTab = document.getElementById('camera-tab');
const fileTab = document.getElementById('file-tab');
if (cameraTab && fileTab) {
cameraTab.addEventListener('click', () => this.switchToCamera());
fileTab.addEventListener('click', () => this.switchToFileUpload());
}
},
/**
* Switch to camera view
*/
switchToCamera: function() {
document.getElementById('camera-tab').classList.add('active');
document.getElementById('file-tab').classList.remove('active');
document.getElementById('camera-container').style.display = 'flex';
document.getElementById('file-container').style.display = 'none';
},
/**
* Switch to file upload view
*/
switchToFileUpload: function() {
document.getElementById('file-tab').classList.add('active');
document.getElementById('camera-tab').classList.remove('active');
document.getElementById('file-container').style.display = 'flex';
document.getElementById('camera-container').style.display = 'none';
},
/**
* Set up event listeners for camera controls
*/
setupCameraControls: function() {
const captureBtn = document.getElementById('capture-button');
if (captureBtn) {
captureBtn.onclick = () => this.captureImage();
}
const uploadBtn = document.getElementById('upload-button');
if (uploadBtn) {
uploadBtn.onclick = () => this.uploadImage();
} else {
console.warn('Upload button not found');
}
const retakeBtn = document.getElementById('retake-button');
if (retakeBtn) {
retakeBtn.onclick = () => this.retakeImage();
}
// Add batch-related controls
const addToBatchBtn = document.getElementById('add-to-batch-btn');
if (addToBatchBtn) {
addToBatchBtn.onclick = () => this.addToBatch();
}
const uploadBatchBtn = document.getElementById('upload-batch-btn');
if (uploadBatchBtn) {
uploadBatchBtn.onclick = () => this.uploadBatch();
}
const clearBatchBtn = document.getElementById('clear-batch-btn');
if (clearBatchBtn) {
clearBatchBtn.onclick = () => this.clearBatch();
}
},
/**
* Set up file upload functionality
*/
setupFileUpload: function() {
const fileInput = document.getElementById('file-input');
const filePreview = document.getElementById('file-preview');
const filePreviewContainer = document.getElementById('file-preview-container');
const cancelFileBtn = document.getElementById('cancel-file-btn');
const uploadFileBtn = document.getElementById('upload-file-btn');
const addFileToBatchBtn = document.getElementById('add-file-to-batch-btn');
if (fileInput) {
fileInput.onchange = (e) => {
const files = e.target.files;
if (files.length > 0) {
// Handle multiple files if supported
if (files.length === 1) {
// Single file - show preview
const file = files[0];
const reader = new FileReader();
reader.onload = (e) => {
filePreview.src = e.target.result;
this.capturedImageData = e.target.result;
filePreviewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
// Multiple files - add all to batch
this.addFilesToBatch(files);
fileInput.value = ''; // Reset input
}
}
};
}
if (cancelFileBtn) {
cancelFileBtn.onclick = () => {
fileInput.value = '';
filePreview.src = '';
this.capturedImageData = null;
filePreviewContainer.style.display = 'none';
};
}
if (uploadFileBtn) {
uploadFileBtn.onclick = () => {
if (this.capturedImageData) {
this.uploadImage();
} else {
Logger.showError("No image selected. Please select an image first.");
}
};
}
if (addFileToBatchBtn) {
addFileToBatchBtn.onclick = () => {
if (this.capturedImageData) {
// Add current file to batch
this.addFileToBatch();
} else {
Logger.showError("No image selected. Please select an image first.");
}
};
}
},
/**
* Add current file to batch
*/
addFileToBatch: function() {
if (!this.capturedImageData) {
Logger.showError("No image selected. Please select an image first.");
return;
}
// Add current image to the batch
this.capturedImages.push(this.capturedImageData);
// Update batch counter
this.updateBatchCounter();
// Add thumbnail to batch preview
this.addThumbnail(this.capturedImageData);
// Show batch preview if it's the first image
if (this.capturedImages.length === 1) {
document.getElementById('batch-preview').classList.add('active');
}
// Reset file input and preview
const fileInput = document.getElementById('file-input');
const filePreview = document.getElementById('file-preview');
const filePreviewContainer = document.getElementById('file-preview-container');
if (fileInput) fileInput.value = '';
if (filePreview) filePreview.src = '';
if (filePreviewContainer) filePreviewContainer.style.display = 'none';
this.capturedImageData = null;
},
/**
* Add multiple files to batch
* @param {FileList} files - List of files to add
*/
addFilesToBatch: function(files) {
if (!files || files.length === 0) return;
// Show loading indicator
const loadingMessage = document.createElement('div');
loadingMessage.className = 'loading-message';
loadingMessage.textContent = 'Processing files...';
document.body.appendChild(loadingMessage);
// Process each file
let processed = 0;
Array.from(files).forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
// Add to batch
this.capturedImages.push(e.target.result);
this.addThumbnail(e.target.result);
processed++;
if (processed === files.length) {
// All files processed
document.body.removeChild(loadingMessage);
// Update counter and show batch
this.updateBatchCounter();
document.getElementById('batch-preview').classList.add('active');
}
};
reader.readAsDataURL(file);
});
},
/**
* Capture an image from the camera
*/
captureImage: function() {
// Capture button clicked
const canvas = document.createElement('canvas');
const video = document.getElementById('camera-view');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
this.capturedImageData = canvas.toDataURL('image/jpeg', 0.9);
document.getElementById('capture-preview').src = this.capturedImageData;
document.querySelector('.container').style.display = 'none';
document.getElementById('review-container').style.display = 'flex';
// Image captured
},
/**
* Add the current image to the batch
*/
addToBatch: function() {
if (!this.capturedImageData) {
Logger.showError("No image captured. Please take a picture first.");
return;
}
// Add current image to the batch
this.capturedImages.push(this.capturedImageData);
// Update batch counter
this.updateBatchCounter();
// Add thumbnail to batch preview
this.addThumbnail(this.capturedImageData);
// Show batch preview if it's the first image
if (this.capturedImages.length === 1) {
document.getElementById('batch-preview').classList.add('active');
}
// Return to camera view
this.retakeImage();
},
/**
* Update the batch counter
*/
updateBatchCounter: function() {
const counter = document.getElementById('batch-counter');
const count = this.capturedImages.length;
counter.textContent = count + (count === 1 ? ' image' : ' images');
},
/**
* Add a thumbnail to the batch preview
* @param {string} dataUrl - Image data URL
*/
addThumbnail: function(dataUrl) {
const container = document.getElementById('batch-thumbnails');
const index = this.capturedImages.length - 1;
const thumbnail = document.createElement('div');
thumbnail.className = 'batch-thumbnail';
thumbnail.dataset.index = index;
const img = document.createElement('img');
img.src = dataUrl;
img.alt = 'Thumbnail';
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-thumbnail';
removeBtn.innerHTML = '×';
removeBtn.onclick = (e) => {
e.stopPropagation();
this.removeFromBatch(index);
};
thumbnail.appendChild(img);
thumbnail.appendChild(removeBtn);
container.appendChild(thumbnail);
},
/**
* Remove an image from the batch
* @param {number} index - Index of the image to remove
*/
removeFromBatch: function(index) {
// Remove from array
this.capturedImages.splice(index, 1);
// Update batch counter
this.updateBatchCounter();
// Refresh thumbnails
this.refreshThumbnails();
// Hide batch preview if no images left
if (this.capturedImages.length === 0) {
document.getElementById('batch-preview').classList.remove('active');
}
},
/**
* Clear all images from the batch
*/
clearBatch: function() {
this.capturedImages = [];
this.updateBatchCounter();
document.getElementById('batch-thumbnails').innerHTML = '';
document.getElementById('batch-preview').classList.remove('active');
},
/**
* Refresh all thumbnails in the batch preview
*/
refreshThumbnails: function() {
const container = document.getElementById('batch-thumbnails');
container.innerHTML = '';
this.capturedImages.forEach((dataUrl, index) => {
this.addThumbnail(dataUrl);
});
},
/**
* Upload all captured images
*/
uploadImage: function() {
// Check if we have images in the batch or just the current preview
if (this.capturedImages.length > 0) {
// Upload all images in the batch
this.uploadBatch();
} else if (this.capturedImageData) {
// Upload just the current image
PeerConnection.sendImage(this.capturedImageData);
} else {
Logger.showError("No images captured. Please take pictures first.");
}
},
/**
* Upload all images in the batch
*/
uploadBatch: function() {
if (this.capturedImages.length === 0) {
Logger.showError("Batch is empty. Please add images first.");
return;
}
// Show spinner
document.getElementById('spinner').style.display = 'flex';
// Send each image sequentially
const totalImages = this.capturedImages.length;
let uploadedCount = 0;
const sendNextImage = (index) => {
if (index >= this.capturedImages.length) {
// All images sent, send completion notification
PeerConnection.connection.send({ type: 'batch-complete' });
// Hide spinner and show success message
setTimeout(() => {
document.getElementById('spinner').style.display = 'none';
document.getElementById('success-message').style.display = 'flex';
// Close window after success
setTimeout(() => window.close(), 3000);
}, 1000);
return;
}
// Update spinner text
const spinnerText = document.querySelector('.spinner-text');
spinnerText.textContent = `Uploading ${index + 1}/${totalImages}`;
// Send image
const dataUrl = this.capturedImages[index];
try {
// Using the peer connection directly for more control
if (PeerConnection.connection && PeerConnection.connection.open) {
PeerConnection.connection.send({ type: 'scan-image', data: dataUrl });
// Wait a bit before sending next image to prevent overwhelming the connection
setTimeout(() => sendNextImage(index + 1), 500);
} else {
// Connection not ready yet, set up connection first
PeerConnection.connection = PeerConnection.peer.connect(PeerConnection.sessionId, { reliable: true });
PeerConnection.connection.on('open', () => {
PeerConnection.connection.send({ type: 'scan-image', data: dataUrl });
setTimeout(() => sendNextImage(index + 1), 500);
});
PeerConnection.connection.on('error', (err) => {
console.error("Connection error:", err);
Logger.showError("Upload failed: " + (err.message || "unknown error"));
// Try to continue with remaining images
setTimeout(() => sendNextImage(index + 1), 1000);
});
}
} catch (err) {
console.error("Send error:", err);
Logger.showError("Failed to send image: " + (err.message || "unknown error"));
// Try to continue with remaining images
setTimeout(() => sendNextImage(index + 1), 1000);
}
};
// Start sending images
sendNextImage(0);
},
/**
* Switch back to camera view for retaking the image
*/
retakeImage: function() {
document.querySelector('.container').style.display = 'flex';
document.getElementById('review-container').style.display = 'none';
// Retake selected
},
/**
* Stop the camera stream
*/
stop: function() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
}
};
export default Camera;

View File

@ -0,0 +1,79 @@
/**
* Utility functions for handling images in the scan upload feature
*/
const ImageUtils = {
/**
* Download all images as a ZIP file
* @param {NodeList} images - Collection of image elements
*/
downloadAllAsZip: function(images) {
if (!images || images.length === 0) {
console.warn('No images to download');
return;
}
// Load JSZip dynamically
const script = document.createElement('script');
script.src = '/js/thirdParty/jszip.min.js';
script.onload = () => {
this.createZip(images);
};
script.onerror = () => {
console.error('Failed to load JSZip');
alert('Failed to load ZIP library. Please try downloading images individually.');
};
document.head.appendChild(script);
},
/**
* Create ZIP file with images
* @param {NodeList} images - Collection of image elements
*/
createZip: function(images) {
try {
// JSZip is now loaded globally
const zip = new JSZip();
const timestamp = new Date().toISOString().slice(0, 10);
// Add each image to the zip
for (let i = 0; i < images.length; i++) {
const img = images[i];
// Extract base64 data from data URL
const dataUrl = img.src;
const base64Data = dataUrl.split(',')[1];
// Add to zip
zip.file(`scan-${i+1}.jpg`, base64Data, {base64: true});
}
// Generate the zip file
zip.generateAsync({type: 'blob'}).then((blob) => {
// Create download link
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `scans-${timestamp}.zip`;
// Trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
} catch (error) {
console.error('Error creating ZIP file:', error);
alert('Failed to create ZIP file. Please try downloading images individually.');
}
},
/**
* Convert a data URL to a Blob
* @param {string} url - Data URL
* @returns {Promise<Blob>} - Promise resolving to a Blob
*/
urlToBlob: function(url) {
return fetch(url).then(response => response.blob());
}
};
export default ImageUtils;

View File

@ -0,0 +1,39 @@
/**
* Simplified logger module for the scan-upload functionality
*/
const Logger = {
/**
* Log a debug message to the console (disabled in production)
* @param {string} message - Message to log
*/
debug: function(message) {
// Debug logging disabled
},
/**
* Update status message in the UI
* @param {string} message - Status message to display
*/
updateStatus: function(message) {
const el = document.getElementById('status-message');
if (el) {
el.textContent = message;
el.style.display = message ? 'block' : 'none';
}
},
/**
* Show an error message
* @param {string} message - Error message to display
*/
showError: function(message) {
this.updateStatus("Error: " + message);
// Only show alerts for critical errors
if (message.includes('failed') || message.includes('denied')) {
alert("Upload Error: " + message);
}
}
};
export default Logger;

View File

@ -0,0 +1,260 @@
/**
* WebRTC peer connection module for scan-upload functionality
*/
import Logger from './logger.js';
const PeerConnection = {
peer: null,
connection: null,
sessionId: null,
isReceiver: false,
/**
* Generate a random session ID
* @returns {string} Random session ID
*/
generateRandomId: function() {
return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, () => (Math.random() * 16 | 0).toString(16));
},
/**
* Initialize a peer connection
* @param {string} id - Peer ID
* @param {boolean} isReceiver - Whether this peer is receiving images
*/
init: function(id, isReceiver) {
this.sessionId = id;
this.isReceiver = isReceiver;
this.peer = new Peer(id, {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com' },
{ urls: 'turn:192.158.29.39:3478?transport=udp', credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', username: '28224511:1379330808' },
{ urls: 'turn:192.158.29.39:3478?transport=tcp', credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', username: '28224511:1379330808' },
{ urls: 'turn:turn.bistri.com:80', credential: 'homeo', username: 'homeo' },
{ urls: 'turn:turn.anyfirewall.com:443?transport=tcp', credential: 'webrtc', username: 'webrtc' }
]
},
debug: 2
});
// Peer created
this.setupEventListeners();
},
/**
* Set up event listeners for the peer connection
*/
setupEventListeners: function() {
this.peer.on('disconnected', () => {
console.warn("PeerJS disconnected");
Logger.showError("Disconnected from PeerJS server");
});
this.peer.on('close', () => {
console.log("PeerJS connection closed");
});
if (this.isReceiver) {
this.peer.on('connection', conn => {
this.connection = conn;
// Receiver connected
conn.on('data', data => {
if (data.type === 'scan-image') {
Logger.updateStatus('Scan received');
document.getElementById('qrcode-container').style.display = 'none';
// Create new image container for each received image
this.addImageToGallery(data.data);
document.querySelector('.scan-result-container').style.display = 'block';
}
else if (data.type === 'batch-complete') {
Logger.updateStatus('Batch upload complete');
}
});
});
}
this.peer.on('error', (err) => {
console.error("PeerJS Error:", err);
// PeerJS error occurred
Logger.showError("PeerJS error: " + err.type + " - " + (err.message || "unknown"));
});
},
/**
* Send an image to the peer
* @param {string} dataUrl - Image data URL
* @param {boolean} closeAfterSend - Whether to close the window after sending (default: true)
*/
sendImage: function(dataUrl, closeAfterSend = true) {
// Attempt to send image
if (!this.connection || !this.connection.open) {
// Establishing connection to peer
this.connection = this.peer.connect(this.sessionId, { reliable: true });
let waited = 0;
const pollInterval = setInterval(() => {
if (this.connection.open) {
clearInterval(pollInterval);
return;
}
waited += 1000;
if (waited >= 10000) {
clearInterval(pollInterval);
console.warn('Connection still not open after 10s. Forcing retry.');
Logger.showError('Connection failed after retry. Check network or switch to same Wi-Fi.');
} else {
// Still waiting for connection
}
}, 1000);
this.connection.on('open', () => {
// Connection opened
this.connection.send({ type: 'scan-image', data: dataUrl });
// Only show success and close window if this is the final image
if (closeAfterSend) {
document.getElementById('spinner').style.display = 'flex';
setTimeout(() => {
document.getElementById('spinner').style.display = 'none';
document.getElementById('success-message').style.display = 'flex';
setTimeout(() => window.close(), 3000);
}, 1000);
}
});
this.connection.on('error', (err) => {
clearInterval(pollInterval);
console.error("Connection error:", err);
// Connection error occurred
Logger.showError("Upload failed: " + (err.message || "unknown error"));
});
} else {
try {
// Using existing connection
this.connection.send({ type: 'scan-image', data: dataUrl });
// Only show success and close window if this is the final image
if (closeAfterSend) {
document.getElementById('spinner').style.display = 'flex';
setTimeout(() => {
document.getElementById('spinner').style.display = 'none';
document.getElementById('success-message').style.display = 'flex';
setTimeout(() => window.close(), 3000);
}, 1000);
}
} catch (err) {
console.error("Send error:", err);
// Send error occurred
Logger.showError("Failed to send image: " + (err.message || "unknown error"));
}
}
},
/**
* Reset the peer connection
*/
reset: function() {
if (this.peer) {
this.peer.destroy();
}
this.connection = null;
},
/**
* Generate a QR code with the session URL
* @param {string} sessionId - Session ID
* @param {HTMLElement} container - QR code container element
*/
generateQRCode: function(sessionId, container) {
const url = `${window.location.origin}/mobile?session=${sessionId}`;
// Clear previous QR code if any
container.innerHTML = '';
new QRCode(container, {
text: url,
width: 256,
height: 256,
colorDark: "#000",
colorLight: "#fff",
correctLevel: QRCode.CorrectLevel.H
});
// Add link below QR code
const a = document.createElement('a');
a.href = url;
a.textContent = url;
a.style.color = '#0af';
a.style.display = 'block';
a.style.marginTop = '10px';
container.appendChild(a);
},
/**
* Add a new image to the gallery
* @param {string} dataUrl - Image data URL
*/
addImageToGallery: function(dataUrl) {
const galleryContainer = document.getElementById('image-gallery');
if (!galleryContainer) {
console.error('Gallery container not found');
return;
}
// Create a new card for the image
const card = document.createElement('div');
card.className = 'image-card';
// Create image element
const img = document.createElement('img');
img.src = dataUrl;
img.className = 'gallery-image';
img.alt = 'Scanned Image';
// Create download button
const downloadBtn = document.createElement('a');
downloadBtn.href = dataUrl;
downloadBtn.className = 'btn btn-primary btn-sm image-action-btn';
downloadBtn.textContent = 'Download';
downloadBtn.download = `scan-${new Date().getTime()}.jpg`;
// Create delete button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-danger btn-sm image-action-btn';
deleteBtn.textContent = 'Remove';
deleteBtn.onclick = function() {
card.remove();
// If all images are removed, hide the gallery
if (galleryContainer.children.length === 0) {
document.querySelector('.scan-result-container').style.display = 'none';
document.getElementById('qrcode-container').style.display = 'block';
}
};
// Create actions container
const actions = document.createElement('div');
actions.className = 'image-actions';
actions.appendChild(downloadBtn);
actions.appendChild(deleteBtn);
// Add all elements to the card
card.appendChild(img);
card.appendChild(actions);
// Add the card to the gallery
galleryContainer.appendChild(card);
}
};
export default PeerConnection;

View File

@ -0,0 +1,83 @@
/**
* Main scan upload module
*/
import Logger from './logger.js';
import PeerConnection from './peer-connection.js';
import Camera from './camera.js';
import ImageUtils from './image-utils.js';
/**
* Initialize scan upload on PC/desktop
*/
function initScanUploadPC() {
// Set as receiver (desktop)
PeerConnection.init(PeerConnection.generateRandomId(), true);
// Display session ID
document.getElementById('session-id').textContent = PeerConnection.sessionId;
// Generate QR code
const qrcodeContainer = document.getElementById('qrcode');
PeerConnection.generateQRCode(PeerConnection.sessionId, qrcodeContainer);
// Set up buttons
document.getElementById('new-scan-btn').onclick = resetConnection;
document.getElementById('download-all-btn').onclick = downloadAllImages;
// Desktop initialized
}
/**
* Initialize scan upload on mobile
*/
function initScanUploadMobile() {
// Initialize mobile app
// Extract session ID from URL
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session');
if (!sessionId) {
Logger.showError('Missing session ID in URL');
return;
}
// Initialize peer connection (not as receiver)
PeerConnection.init(PeerConnection.generateRandomId(), false);
PeerConnection.sessionId = sessionId;
// Initialize camera
Camera.init();
// Mobile initialized
}
/**
* Reset the connection
*/
function resetConnection() {
PeerConnection.reset();
initScanUploadPC();
}
/**
* Download all images as a ZIP
*/
function downloadAllImages() {
const images = document.querySelectorAll('.gallery-image');
if (images.length === 0) {
alert('No images to download');
return;
}
ImageUtils.downloadAllAsZip(images);
}
// Export functions for global use
window.initScanUploadPC = initScanUploadPC;
window.initScanUploadMobile = initScanUploadMobile;
// Initialize based on page load
document.addEventListener('DOMContentLoaded', () => {
// This will be initialized from the HTML page based on which view is loaded
});

View File

@ -0,0 +1,7 @@
/*!
* EventEmitter v5.2.9 - git.io/ee
* Unlicense - http://unlicense.org/
* Oliver Caldwell - https://oli.me.uk/
* @preserve
*/
!function(e){"use strict";function t(){}function n(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function r(e){return function(){return this[e].apply(this,arguments)}}function i(e){return"function"==typeof e||e instanceof RegExp||!(!e||"object"!=typeof e)&&i(e.listener)}var o=t.prototype,s=e.EventEmitter;o.getListeners=function(e){var t,n,r=this._getEvents();if(e instanceof RegExp){t={};for(n in r)r.hasOwnProperty(n)&&e.test(n)&&(t[n]=r[n])}else t=r[e]||(r[e]=[]);return t},o.flattenListeners=function(e){var t,n=[];for(t=0;t<e.length;t+=1)n.push(e[t].listener);return n},o.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},o.addListener=function(e,t){if(!i(t))throw new TypeError("listener must be a function");var r,o=this.getListenersAsObject(e),s="object"==typeof t;for(r in o)o.hasOwnProperty(r)&&-1===n(o[r],t)&&o[r].push(s?t:{listener:t,once:!1});return this},o.on=r("addListener"),o.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},o.once=r("addOnceListener"),o.defineEvent=function(e){return this.getListeners(e),this},o.defineEvents=function(e){for(var t=0;t<e.length;t+=1)this.defineEvent(e[t]);return this},o.removeListener=function(e,t){var r,i,o=this.getListenersAsObject(e);for(i in o)o.hasOwnProperty(i)&&-1!==(r=n(o[i],t))&&o[i].splice(r,1);return this},o.off=r("removeListener"),o.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},o.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},o.manipulateListeners=function(e,t,n){var r,i,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(r=n.length;r--;)o.call(this,t,n[r]);else for(r in t)t.hasOwnProperty(r)&&(i=t[r])&&("function"==typeof i?o.call(this,r,i):s.call(this,r,i));return this},o.removeEvent=function(e){var t,n=typeof e,r=this._getEvents();if("string"===n)delete r[e];else if(e instanceof RegExp)for(t in r)r.hasOwnProperty(t)&&e.test(t)&&delete r[t];else delete this._events;return this},o.removeAllListeners=r("removeEvent"),o.emitEvent=function(e,t){var n,r,i,o,s=this.getListenersAsObject(e);for(o in s)if(s.hasOwnProperty(o))for(n=s[o].slice(0),i=0;i<n.length;i++)r=n[i],!0===r.once&&this.removeListener(e,r.listener),r.listener.apply(this,t||[])===this._getOnceReturnValue()&&this.removeListener(e,r.listener);return this},o.trigger=r("emitEvent"),o.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},o.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},o._getOnceReturnValue=function(){return!this.hasOwnProperty("_onceReturnValue")||this._onceReturnValue},o._getEvents=function(){return this._events||(this._events={})},t.noConflict=function(){return e.EventEmitter=s,t},"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:e.EventEmitter=t}(this||{});

View File

@ -0,0 +1,50 @@
/**
* PeerJS Wrapper - Simple initialization helper
*/
// Check if PeerJS is loaded properly
if (typeof Peer === 'undefined') {
console.error('PeerJS is not defined. Make sure peerjs.min.js is loaded before this script.');
} else {
console.log('PeerJS loaded successfully, version:', Peer.version || 'unknown');
}
// Create a global connection manager
window.PeerManager = {
createPeer: function(id, options) {
try {
// Default options with cloud server
var defaultOptions = {};
// Merge options
var finalOptions = options ? Object.assign({}, defaultOptions, options) : defaultOptions;
console.log('Creating PeerJS instance with ID:', id);
return new Peer(id, finalOptions);
} catch (err) {
console.error('Error creating PeerJS instance:', err);
// Return a dummy peer that won't crash the application
return {
id: id,
on: function(event, callback) {
if (event === 'error') {
setTimeout(function() {
callback(new Error('Could not create PeerJS instance'));
}, 100);
}
return this;
},
connect: function() {
return {
on: function() { return this; },
send: function() {}
};
},
destroy: function() {}
};
}
}
};
console.log('PeerJS wrapper initialized successfully');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -277,6 +277,9 @@
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-pdf-by-chapters', '/images/split-chapters.svg#icon-split-chapters', 'home.splitPdfByChapters.title', 'home.splitPdfByChapters.desc', 'splitPdfByChapters.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('scan-upload', 'qr_code_scanner', 'home.scan-upload.title', 'home.scan-upload.desc', 'scan-upload.tags', 'advance')}">
</div>
</div>
</div>
</th:block>

View File

@ -1,5 +1,5 @@
<div th:fragment="navbar" class="mx-auto" style="position: sticky; top:0; z-index:10000; width:100%">
<script th:src="@{'/js/languageSelection.js'}"></script>
<script th:src="@{'/js/languageSelection.js'}"></script>
<script th:src="@{'/js/navbar.js'}"></script>
<script th:src="@{'/js/additionalLanguageCode.js'}"></script>
<script th:inline="javascript">

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{mobile.title}, header=#{mobile.title})}"></th:block>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" th:href="@{'/css/mobile-scanner.css'}">
<script th:src="@{'/js/thirdParty/peerjs.min.js'}"></script>
<script type="module" th:src="@{'/js/scan-upload/scan-upload.js'}"></script>
</head>
<body>
<div class="app-header">
<div class="input-method-tabs">
<button id="camera-tab" class="input-tab active">Camera</button>
<button id="file-tab" class="input-tab">File Upload</button>
</div>
</div>
<div id="camera-container" class="container">
<div class="camera-container">
<video id="camera-view" autoplay playsinline></video>
</div>
<div class="controls">
<div class="capture-btn" id="capture-button" th:title="#{mobile.align-document}"></div>
</div>
</div>
<div id="file-container" class="container" style="display:none;">
<div class="file-upload-container">
<label for="file-input" class="file-upload-label">
<span class="upload-icon">+</span>
<span class="upload-text">Select Image(s)</span>
</label>
<input type="file" id="file-input" accept="image/*" multiple style="display:none;">
<div id="file-preview-container" class="file-preview-container" style="display:none;">
<img id="file-preview" class="file-preview" src="" alt="File Preview">
<div class="file-actions">
<button id="cancel-file-btn" class="btn btn-secondary">Cancel</button>
<button id="add-file-to-batch-btn" class="btn btn-success">Add to Batch</button>
<button id="upload-file-btn" class="btn btn-primary">Upload</button>
</div>
</div>
</div>
</div>
<div class="review-container" id="review-container">
<div class="preview-header">
<h3>Preview</h3>
<div class="preview-controls">
<button id="add-to-batch-btn" class="batch-btn">Add to Batch</button>
<span id="batch-counter" class="batch-counter">0 images</span>
</div>
</div>
<img id="capture-preview" src="" alt="Preview">
<div class="review-controls">
<button class="review-btn retake-btn" id="retake-button" th:text="#{mobile.retake}">Retake</button>
<button class="review-btn upload-btn" id="upload-button" th:text="#{mobile.upload}">Upload All</button>
</div>
</div>
<div class="batch-preview" id="batch-preview">
<div class="batch-header">
<h4>Batch Images</h4>
<div class="batch-actions">
<button id="upload-batch-btn" class="upload-batch-btn">Upload All</button>
<button id="clear-batch-btn" class="clear-btn">Clear All</button>
</div>
</div>
<div id="batch-thumbnails" class="batch-thumbnails"></div>
</div>
<div class="spinner" id="spinner">
<div class="spinner-border"></div>
<div class="spinner-text" th:text="#{mobile.uploading}">Uploading...</div>
</div>
<div class="success-message" id="success-message">
<i class="bi bi-check-circle-fill"></i>
<h2 th:text="#{mobile.success}">Success!</h2>
<p th:text="#{mobile.success-message}">Scan uploaded</p>
</div>
<div class="camera-error" id="camera-error">
<i class="bi bi-camera-video-off" style="font-size: 48px; margin-bottom: 20px;"></i>
<h2 th:text="#{mobile.camera-access}">Camera Access Required</h2>
<p th:text="#{mobile.camera-permission}">Please allow camera access and reload this page over HTTPS.</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof initScanUploadMobile === 'function') {
initScanUploadMobile();
// Ensure debug log div is visible if script didn't add it
const log = document.getElementById('mobile-debug-log');
if (log) log.style.display = 'block';
} else {
console.error('ScanUpload module not loaded correctly');
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{scan-upload.title}, header=#{scan-upload.title})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/scan-upload.css'}">
<script th:src="@{'/js/thirdParty/qrcode.min.js'}"></script>
<script th:src="@{'/js/thirdParty/peerjs.min.js'}"></script>
<script type="module" th:src="@{'/js/scan-upload/scan-upload.js'}"></script>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="container scan-container">
<div class="card mt-5">
<div class="card-header">
<h2 th:text="#{scan-upload.title}">Scan and Upload</h2>
</div>
<div class="card-body">
<div class="alert alert-info scan-info">
<p th:text="#{scan-upload.description}">Scan with your phone and upload directly to this browser.</p>
</div>
<div id="status-message" class="status-msg"></div>
<div id="qrcode-container">
<h4 th:text="#{scan-upload.instructions.title}">Scan this QR code with your phone</h4>
<div id="qrcode"></div>
<div class="session-id"><span th:text="#{scan-upload.session}">Session ID:</span> <span id="session-id"></span></div>
</div>
<div class="scan-result-container" style="display:none">
<h4 th:text="#{scan-upload.result}">Scan Results</h4>
<div id="image-gallery" class="image-gallery"></div>
<div class="actions">
<button id="new-scan-btn" class="btn btn-secondary" th:text="#{scan-upload.new-scan}">New Scan</button>
<a id="download-all-btn" class="btn btn-primary" >Download All (ZIP)</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof initScanUploadPC === 'function') {
initScanUploadPC();
} else {
console.error('ScanUpload module not loaded correctly');
}
});
</script>
</body>
</html>