Replace file-input capture with live camera viewfinder
The previous approach used setTimeout to re-trigger the file input after each capture, which iOS Safari blocks because it loses the user gesture context. Now uses getUserMedia for a fullscreen camera viewfinder that stays open between shots. Shutter button captures frames from the video stream, saves to IndexedDB, and the camera remains open until the user taps close. Includes flash feedback, session counter, and camera flip. Falls back to single-shot file input if getUserMedia is unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
📷 Take Photo
|
📷 Take Photo
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Hidden file input as fallback -->
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="camera-input"
|
id="camera-input"
|
||||||
@@ -21,11 +22,6 @@
|
|||||||
capture="environment"
|
capture="environment"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Session counter shown during continuous capture -->
|
|
||||||
<div class="capture-session-bar" id="capture-session-bar" style="display:none">
|
|
||||||
<span id="session-count">0</span> photos this session
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recent-photos" id="recent-photos">
|
<div class="recent-photos" id="recent-photos">
|
||||||
@@ -34,9 +30,34 @@
|
|||||||
<p class="empty-state">No photos captured yet</p>
|
<p class="empty-state">No photos captured yet</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Capture toast -->
|
<!-- Fullscreen Camera Viewfinder -->
|
||||||
<div class="capture-toast" id="capture-toast"></div>
|
<div class="camera-viewfinder" id="camera-viewfinder" style="display:none">
|
||||||
|
<video id="camera-video" autoplay playsinline muted></video>
|
||||||
|
|
||||||
|
<!-- Flash overlay for shutter feedback -->
|
||||||
|
<div class="camera-flash" id="camera-flash"></div>
|
||||||
|
|
||||||
|
<div class="camera-top-bar">
|
||||||
|
<button class="camera-ui-btn camera-close-btn" id="camera-close-btn">×</button>
|
||||||
|
<span class="camera-counter" id="camera-counter"></span>
|
||||||
|
<div style="width:48px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="camera-bottom-bar">
|
||||||
|
<div style="width:48px"></div>
|
||||||
|
<button class="camera-shutter-btn" id="camera-shutter-btn">
|
||||||
|
<span class="shutter-ring"></span>
|
||||||
|
</button>
|
||||||
|
<button class="camera-ui-btn camera-flip-btn" id="camera-flip-btn">🔄</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden canvas for frame capture -->
|
||||||
|
<canvas id="camera-canvas" style="display:none"></canvas>
|
||||||
|
|
||||||
|
<!-- Toast inside viewfinder -->
|
||||||
|
<div class="camera-toast" id="camera-toast"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -77,33 +98,6 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capture-session-bar {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--accent);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.capture-toast {
|
|
||||||
position: fixed;
|
|
||||||
top: 70px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: 0.6rem 1.25rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
z-index: 9998;
|
|
||||||
display: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: rgba(40, 167, 69, 0.9);
|
|
||||||
color: #fff;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-photos {
|
.recent-photos {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
@@ -164,6 +158,142 @@
|
|||||||
.thumbnail .status-badge.verified {
|
.thumbnail .status-badge.verified {
|
||||||
background: var(--success);
|
background: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Camera Viewfinder */
|
||||||
|
body.camera-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-viewfinder {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: #000;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-viewfinder video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-flash {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
transition: opacity 0.05s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-flash.flash {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-top-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
padding-top: max(0.5rem, env(safe-area-inset-top));
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-ui-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-close-btn {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-counter {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-bottom-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-shutter-btn {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-shutter-btn:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-shutter-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutter-ring {
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-flip-btn {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 140px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
z-index: 4;
|
||||||
|
display: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgba(40, 167, 69, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -191,111 +321,194 @@ Storage.init().then(() => {
|
|||||||
const captureBtn = document.getElementById('capture-btn');
|
const captureBtn = document.getElementById('capture-btn');
|
||||||
const cameraInput = document.getElementById('camera-input');
|
const cameraInput = document.getElementById('camera-input');
|
||||||
|
|
||||||
// Continuous capture state
|
// ---- Camera state ----
|
||||||
|
let cameraStream = null;
|
||||||
|
let facingMode = 'environment';
|
||||||
let sessionCount = 0;
|
let sessionCount = 0;
|
||||||
let continuousCapture = false;
|
let shutterBusy = false;
|
||||||
let processingPhoto = false;
|
|
||||||
|
|
||||||
captureBtn.addEventListener('click', function() {
|
// ---- Main button: open live viewfinder, fallback to file input ----
|
||||||
sessionCount = 0;
|
captureBtn.addEventListener('click', async function() {
|
||||||
updateSessionCounter();
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||||
cameraInput.click();
|
await openCamera();
|
||||||
|
} else {
|
||||||
|
// Fallback: use file input
|
||||||
|
cameraInput.click();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- File input fallback (single-shot) ----
|
||||||
cameraInput.addEventListener('change', async function(e) {
|
cameraInput.addEventListener('change', async function(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) {
|
if (!file) return;
|
||||||
// User cancelled the camera — stop continuous capture
|
|
||||||
continuousCapture = false;
|
|
||||||
if (sessionCount > 0) {
|
|
||||||
showCaptureToast(sessionCount + ' photo' + (sessionCount !== 1 ? 's' : '') + ' captured');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
processingPhoto = true;
|
|
||||||
captureBtn.disabled = true;
|
captureBtn.disabled = true;
|
||||||
captureBtn.textContent = '⏳ Processing...';
|
captureBtn.textContent = '⏳ Processing...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jpegBlob = await convertToJPEG(file);
|
const jpegBlob = await convertToJPEG(file);
|
||||||
|
await savePhoto(jpegBlob);
|
||||||
const now = new Date();
|
captureBtn.textContent = '✓ Photo Saved!';
|
||||||
const timestamp = now.getFullYear() +
|
loadRecentPhotos();
|
||||||
String(now.getMonth() + 1).padStart(2, '0') +
|
|
||||||
String(now.getDate()).padStart(2, '0') + '_' +
|
|
||||||
String(now.getHours()).padStart(2, '0') +
|
|
||||||
String(now.getMinutes()).padStart(2, '0') +
|
|
||||||
String(now.getSeconds()).padStart(2, '0');
|
|
||||||
const filename = currentUsername + '_' + timestamp + '.jpg';
|
|
||||||
const targetPath = localStorage.getItem('nextsnap_upload_path') || '/';
|
|
||||||
|
|
||||||
if (storageReady) {
|
|
||||||
await Storage.savePhoto({
|
|
||||||
username: currentUsername,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
filename: filename,
|
|
||||||
targetPath: targetPath,
|
|
||||||
blob: jpegBlob,
|
|
||||||
status: 'pending'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (navigator.onLine && typeof SyncEngine !== 'undefined') {
|
|
||||||
SyncEngine.triggerSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRecentPhotos();
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionCount++;
|
|
||||||
continuousCapture = true;
|
|
||||||
updateSessionCounter();
|
|
||||||
showCaptureToast('Photo saved');
|
|
||||||
|
|
||||||
captureBtn.disabled = false;
|
|
||||||
captureBtn.textContent = '📷 Take Photo';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
captureBtn.textContent = '❌ Error';
|
|
||||||
continuousCapture = false;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
captureBtn.disabled = false;
|
captureBtn.disabled = false;
|
||||||
captureBtn.textContent = '📷 Take Photo';
|
captureBtn.textContent = '📷 Take Photo';
|
||||||
}, 2000);
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
alert('Failed: ' + error.message);
|
alert('Failed: ' + error.message);
|
||||||
|
captureBtn.disabled = false;
|
||||||
|
captureBtn.textContent = '📷 Take Photo';
|
||||||
}
|
}
|
||||||
|
|
||||||
processingPhoto = false;
|
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
|
|
||||||
// Re-open camera automatically for next photo
|
|
||||||
if (continuousCapture) {
|
|
||||||
setTimeout(() => {
|
|
||||||
cameraInput.click();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateSessionCounter() {
|
// ---- Live camera viewfinder ----
|
||||||
const bar = document.getElementById('capture-session-bar');
|
async function openCamera() {
|
||||||
const countEl = document.getElementById('session-count');
|
sessionCount = 0;
|
||||||
if (sessionCount > 0) {
|
updateCameraCounter();
|
||||||
countEl.textContent = sessionCount;
|
|
||||||
bar.style.display = 'inline-block';
|
try {
|
||||||
} else {
|
cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||||
bar.style.display = 'none';
|
video: { facingMode: facingMode, width: { ideal: 3840 }, height: { ideal: 2160 } },
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const video = document.getElementById('camera-video');
|
||||||
|
video.srcObject = cameraStream;
|
||||||
|
await video.play();
|
||||||
|
|
||||||
|
document.getElementById('camera-viewfinder').style.display = 'flex';
|
||||||
|
document.body.classList.add('camera-open');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getUserMedia failed:', err);
|
||||||
|
// Fallback to file input
|
||||||
|
cameraInput.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCaptureToast(msg) {
|
function closeCamera() {
|
||||||
const toast = document.getElementById('capture-toast');
|
if (cameraStream) {
|
||||||
toast.textContent = '✓ ' + msg;
|
cameraStream.getTracks().forEach(t => t.stop());
|
||||||
|
cameraStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = document.getElementById('camera-video');
|
||||||
|
video.srcObject = null;
|
||||||
|
|
||||||
|
document.getElementById('camera-viewfinder').style.display = 'none';
|
||||||
|
document.body.classList.remove('camera-open');
|
||||||
|
|
||||||
|
loadRecentPhotos();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flipCamera() {
|
||||||
|
facingMode = facingMode === 'environment' ? 'user' : 'environment';
|
||||||
|
|
||||||
|
if (cameraStream) {
|
||||||
|
cameraStream.getTracks().forEach(t => t.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: facingMode, width: { ideal: 3840 }, height: { ideal: 2160 } },
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
const video = document.getElementById('camera-video');
|
||||||
|
video.srcObject = cameraStream;
|
||||||
|
await video.play();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Flip camera failed:', err);
|
||||||
|
showCameraToast('Could not switch camera');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takePhoto() {
|
||||||
|
if (shutterBusy) return;
|
||||||
|
shutterBusy = true;
|
||||||
|
|
||||||
|
const shutterBtn = document.getElementById('camera-shutter-btn');
|
||||||
|
shutterBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const video = document.getElementById('camera-video');
|
||||||
|
const canvas = document.getElementById('camera-canvas');
|
||||||
|
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(video, 0, 0);
|
||||||
|
|
||||||
|
// Flash feedback
|
||||||
|
const flash = document.getElementById('camera-flash');
|
||||||
|
flash.classList.add('flash');
|
||||||
|
setTimeout(() => flash.classList.remove('flash'), 150);
|
||||||
|
|
||||||
|
const blob = await new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob(b => b ? resolve(b) : reject(new Error('Canvas capture failed')), 'image/jpeg', 0.92);
|
||||||
|
});
|
||||||
|
|
||||||
|
await savePhoto(blob);
|
||||||
|
sessionCount++;
|
||||||
|
updateCameraCounter();
|
||||||
|
showCameraToast('Photo saved');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('takePhoto error:', err);
|
||||||
|
showCameraToast('Error saving photo');
|
||||||
|
} finally {
|
||||||
|
shutterBusy = false;
|
||||||
|
shutterBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Wire viewfinder buttons ----
|
||||||
|
document.getElementById('camera-close-btn').addEventListener('click', closeCamera);
|
||||||
|
document.getElementById('camera-shutter-btn').addEventListener('click', takePhoto);
|
||||||
|
document.getElementById('camera-flip-btn').addEventListener('click', flipCamera);
|
||||||
|
|
||||||
|
function updateCameraCounter() {
|
||||||
|
const el = document.getElementById('camera-counter');
|
||||||
|
el.textContent = sessionCount > 0 ? sessionCount + ' photo' + (sessionCount !== 1 ? 's' : '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCameraToast(msg) {
|
||||||
|
const toast = document.getElementById('camera-toast');
|
||||||
|
toast.textContent = msg;
|
||||||
toast.style.display = 'block';
|
toast.style.display = 'block';
|
||||||
setTimeout(() => { toast.style.display = 'none'; }, 1500);
|
setTimeout(() => { toast.style.display = 'none'; }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10MB
|
// ---- Shared helpers ----
|
||||||
|
async function savePhoto(jpegBlob) {
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.getFullYear() +
|
||||||
|
String(now.getMonth() + 1).padStart(2, '0') +
|
||||||
|
String(now.getDate()).padStart(2, '0') + '_' +
|
||||||
|
String(now.getHours()).padStart(2, '0') +
|
||||||
|
String(now.getMinutes()).padStart(2, '0') +
|
||||||
|
String(now.getSeconds()).padStart(2, '0');
|
||||||
|
const filename = currentUsername + '_' + timestamp + '.jpg';
|
||||||
|
const targetPath = localStorage.getItem('nextsnap_upload_path') || '/';
|
||||||
|
|
||||||
|
if (storageReady) {
|
||||||
|
await Storage.savePhoto({
|
||||||
|
username: currentUsername,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
filename: filename,
|
||||||
|
targetPath: targetPath,
|
||||||
|
blob: jpegBlob,
|
||||||
|
status: 'pending'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navigator.onLine && typeof SyncEngine !== 'undefined') {
|
||||||
|
SyncEngine.triggerSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
function convertToJPEG(file) {
|
function convertToJPEG(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -318,10 +531,8 @@ function convertToJPEG(file) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (blob.size <= MAX_UPLOAD_BYTES || quality <= 0.3) {
|
if (blob.size <= MAX_UPLOAD_BYTES || quality <= 0.3) {
|
||||||
console.log('Photo: ' + (blob.size / 1024 / 1024).toFixed(1) + 'MB, ' + canvas.width + 'x' + canvas.height + ', q=' + quality.toFixed(2));
|
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
} else {
|
} else {
|
||||||
console.log('Photo too large (' + (blob.size / 1024 / 1024).toFixed(1) + 'MB), reducing...');
|
|
||||||
if (quality > 0.5) {
|
if (quality > 0.5) {
|
||||||
quality -= 0.1;
|
quality -= 0.1;
|
||||||
} else {
|
} else {
|
||||||
@@ -368,40 +579,5 @@ async function loadRecentPhotos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Capture page loaded');
|
console.log('Capture page loaded');
|
||||||
|
|
||||||
// Debug panel
|
|
||||||
setTimeout(() => {
|
|
||||||
const debugDiv = document.createElement('div');
|
|
||||||
debugDiv.id = 'debug-panel';
|
|
||||||
debugDiv.style.cssText = 'position:fixed;bottom:80px;right:10px;z-index:9999;background:black;color:lime;padding:10px;border-radius:5px;font-family:monospace;font-size:10px;max-width:300px;';
|
|
||||||
|
|
||||||
const status = [];
|
|
||||||
status.push('Storage: ' + (typeof Storage !== 'undefined' ? 'OK' : 'FAIL'));
|
|
||||||
status.push('SyncEngine: ' + (typeof SyncEngine !== 'undefined' ? 'OK' : 'FAIL'));
|
|
||||||
status.push('Dexie: ' + (typeof Dexie !== 'undefined' ? 'OK' : 'FAIL'));
|
|
||||||
|
|
||||||
debugDiv.innerHTML = '<div style="margin-bottom:5px;">' + status.join('<br>') + '</div>' +
|
|
||||||
'<button onclick="manualSync()" style="background:lime;color:black;border:none;padding:10px;border-radius:5px;cursor:pointer;font-weight:bold;width:100%;">FORCE SYNC</button>' +
|
|
||||||
'<div id="sync-status" style="margin-top:5px;color:yellow;"></div>';
|
|
||||||
document.body.appendChild(debugDiv);
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
window.manualSync = function() {
|
|
||||||
const statusDiv = document.getElementById('sync-status');
|
|
||||||
statusDiv.textContent = 'Checking...';
|
|
||||||
|
|
||||||
if (typeof SyncEngine === 'undefined') {
|
|
||||||
statusDiv.textContent = 'ERROR: SyncEngine not loaded!';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusDiv.textContent = 'Triggering sync...';
|
|
||||||
try {
|
|
||||||
SyncEngine.triggerSync();
|
|
||||||
statusDiv.textContent = 'Sync triggered!';
|
|
||||||
} catch (e) {
|
|
||||||
statusDiv.textContent = 'ERROR: ' + e.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user