feat: Add webcam recording task overlay system
- Implement canvas-based video compositing for task overlays - Task information now burned directly into webcam recordings - Show actual task content instead of generic descriptions - Remove screen overlay in favor of webcam-embedded overlay - Auto-enable task overlay for all webcam recordings - Add real-time task text, type, and timer display on recordings - Enhance training session documentation and review capabilities
This commit is contained in:
parent
676f06b2f1
commit
bb27735b46
File diff suppressed because it is too large
Load Diff
36
index.html
36
index.html
|
|
@ -187,6 +187,10 @@
|
|||
<span class="feature-icon">🏆</span>
|
||||
<span class="feature-text">Porn Cinema</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="hypno-gallery-btn">
|
||||
<span class="feature-icon">🌀</span>
|
||||
<span class="feature-text">Hypno Gallery</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="library-btn">
|
||||
<span class="cassie-icon"></span>
|
||||
<span class="feature-text">Library</span>
|
||||
|
|
@ -316,6 +320,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">🌀</div>
|
||||
<div class="guide-info">
|
||||
<h4>Hypno Gallery</h4>
|
||||
<p>Immersive slideshow experience with configurable timing modes, visual effects, and transitions. View your image library in constant, random, or wave timing patterns with customizable durations and effects.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">📁</div>
|
||||
<div class="guide-info">
|
||||
|
|
@ -1641,12 +1653,22 @@
|
|||
// Setup backup button handlers
|
||||
setupBackupHandlers();
|
||||
|
||||
// Create initial backup
|
||||
// Create initial backup with retry logic
|
||||
try {
|
||||
window.backupManager.createBackup(false);
|
||||
console.log('🛡️ Initial backup created on startup');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Initial backup failed:', error);
|
||||
console.warn('⚠️ Initial backup failed, attempting cleanup and retry:', error);
|
||||
try {
|
||||
// Perform emergency cleanup and try minimal backup
|
||||
window.backupManager.performEmergencyCleanup();
|
||||
const minimalBackup = window.backupManager.createMinimalBackup(false);
|
||||
const backupKey = `${window.backupManager.backupPrefix}${Date.now()}`;
|
||||
localStorage.setItem(backupKey, JSON.stringify(minimalBackup));
|
||||
console.log('🛡️ Minimal backup created after cleanup');
|
||||
} catch (retryError) {
|
||||
console.error('❌ Even minimal backup failed:', retryError);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ BackupManager not available');
|
||||
|
|
@ -3595,7 +3617,15 @@
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
// Set up hypno gallery button (only once)
|
||||
const hypnoGalleryBtn = document.getElementById('hypno-gallery-btn');
|
||||
if (hypnoGalleryBtn && !hypnoGalleryBtn.hasAttribute('data-handler-attached')) {
|
||||
hypnoGalleryBtn.setAttribute('data-handler-attached', 'true');
|
||||
hypnoGalleryBtn.addEventListener('click', () => {
|
||||
console.log('🌀 Opening Hypno Gallery...');
|
||||
window.location.href = 'hypno-gallery.html';
|
||||
});
|
||||
}
|
||||
|
||||
// Set up user profile button (only once)
|
||||
const userProfileBtn = document.getElementById('user-profile-btn');
|
||||
|
|
|
|||
518
quick-play.html
518
quick-play.html
|
|
@ -27,6 +27,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Quick Play Header -->
|
||||
<header class="quick-play-header">
|
||||
<div class="quick-play-nav">
|
||||
|
|
@ -52,6 +54,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="nav-right quick-play-controls">
|
||||
|
||||
<button id="back-to-home" class="btn btn-secondary">🏠 Home</button>
|
||||
<button id="quick-play-webcam-btn" class="btn btn-info" title="Take a photo with webcam">📸 Photo</button>
|
||||
<button id="force-exit" class="btn btn-danger" title="Force close application">❌ Force Exit</button>
|
||||
|
|
@ -1410,6 +1413,8 @@
|
|||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
console.log('✅ Quick Play initialization complete');
|
||||
|
||||
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -2079,9 +2084,234 @@
|
|||
console.log('📋 Video setup notification shown to user');
|
||||
}
|
||||
|
||||
// Recording Overlay System - Always enabled for webcam recording
|
||||
let recordingOverlayEnabled = true;
|
||||
let recordingStartTime = Date.now();
|
||||
let recordingTimerInterval = null;
|
||||
|
||||
function toggleRecordingOverlay() {
|
||||
const webcamViewer = document.getElementById('webcam-viewer');
|
||||
const button = document.getElementById('toggle-recording-overlay');
|
||||
|
||||
console.log('🎬 Toggle recording overlay called. Current state:', recordingOverlayEnabled);
|
||||
console.log('🎬 Webcam viewer found:', !!webcamViewer);
|
||||
console.log('🎬 Button element found:', !!button);
|
||||
|
||||
if (!recordingOverlayEnabled) {
|
||||
// Enable webcam task overlay
|
||||
recordingOverlayEnabled = true;
|
||||
recordingStartTime = Date.now();
|
||||
|
||||
// Add recording class to webcam viewer to show task overlay
|
||||
if (webcamViewer) {
|
||||
webcamViewer.classList.add('recording');
|
||||
console.log('🎬 Added recording class to webcam viewer');
|
||||
}
|
||||
|
||||
button.textContent = '🔴 Task Overlay';
|
||||
button.classList.remove('btn-info');
|
||||
button.classList.add('btn-danger');
|
||||
|
||||
// Start timer
|
||||
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
|
||||
|
||||
// Update with current task
|
||||
updateRecordingOverlay();
|
||||
|
||||
console.log('🎬 Webcam task overlay enabled');
|
||||
} else {
|
||||
// Disable webcam task overlay
|
||||
recordingOverlayEnabled = false;
|
||||
recordingStartTime = null;
|
||||
|
||||
// Remove recording class from webcam viewer to hide task overlay
|
||||
if (webcamViewer) {
|
||||
webcamViewer.classList.remove('recording');
|
||||
console.log('🎬 Removed recording class from webcam viewer');
|
||||
}
|
||||
|
||||
button.textContent = '🎬 Task Overlay';
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-info');
|
||||
|
||||
// Stop timer
|
||||
if (recordingTimerInterval) {
|
||||
clearInterval(recordingTimerInterval);
|
||||
recordingTimerInterval = null;
|
||||
}
|
||||
|
||||
console.log('🎬 Webcam task overlay disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function updateRecordingTimer() {
|
||||
if (!recordingStartTime) return;
|
||||
|
||||
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
// Update both screen overlay and webcam overlay timestamps
|
||||
const timestampElement = document.getElementById('recording-timestamp');
|
||||
if (timestampElement) {
|
||||
timestampElement.textContent = timeString;
|
||||
}
|
||||
|
||||
// Update webcam overlay timer if no task timer is active
|
||||
const webcamTimer = document.getElementById('webcam-task-timer');
|
||||
const taskTimer = document.getElementById('task-timer');
|
||||
if (webcamTimer && (!taskTimer || !taskTimer.textContent || taskTimer.textContent === '0:00')) {
|
||||
webcamTimer.textContent = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRecordingOverlay() {
|
||||
if (!recordingOverlayEnabled) {
|
||||
console.log('🎬 updateRecordingOverlay called but recording not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🎬 Updating recording overlay content');
|
||||
|
||||
// Try multiple selectors to find task elements
|
||||
let taskText = document.getElementById('task-text');
|
||||
let taskTitle = document.getElementById('task-title');
|
||||
let taskTimer = document.getElementById('task-timer');
|
||||
|
||||
// Fallback selectors if main ones not found
|
||||
if (!taskText) taskText = document.querySelector('.task-text, [data-task-text]');
|
||||
if (!taskTitle) taskTitle = document.querySelector('.task-title, [data-task-title], .current-task-title');
|
||||
if (!taskTimer) taskTimer = document.querySelector('.task-timer, [data-task-timer], .timer-display');
|
||||
|
||||
console.log('🎬 Main task elements found:', {
|
||||
taskText: !!taskText,
|
||||
taskTitle: !!taskTitle,
|
||||
taskTimer: !!taskTimer
|
||||
});
|
||||
|
||||
const overlayTaskType = document.getElementById('webcam-task-type');
|
||||
const overlayTaskText = document.getElementById('webcam-task-text');
|
||||
const overlayTimer = document.getElementById('webcam-task-timer');
|
||||
|
||||
console.log('🎬 Webcam overlay elements found:', {
|
||||
overlayTaskType: !!overlayTaskType,
|
||||
overlayTaskText: !!overlayTaskText,
|
||||
overlayTimer: !!overlayTimer
|
||||
});
|
||||
|
||||
// Get task information from DOM or game state
|
||||
let taskTitleText = '';
|
||||
let taskTextContent = '';
|
||||
let taskTimerContent = '';
|
||||
|
||||
if (taskTitle) {
|
||||
taskTitleText = taskTitle.textContent || '';
|
||||
} else if (window.gameInstance && window.gameInstance.gameState && window.gameInstance.gameState.currentTask) {
|
||||
taskTitleText = window.gameInstance.gameState.currentTask.title || window.gameInstance.gameState.currentTask.text || '';
|
||||
}
|
||||
|
||||
// Get actual task text - prefer title over description for actual task content
|
||||
if (taskTitle) {
|
||||
taskTextContent = taskTitle.textContent || '';
|
||||
} else if (taskText) {
|
||||
taskTextContent = taskText.textContent || '';
|
||||
} else if (window.gameInstance && window.gameInstance.gameState && window.gameInstance.gameState.currentTask) {
|
||||
taskTextContent = window.gameInstance.gameState.currentTask.title || window.gameInstance.gameState.currentTask.text || 'Training session in progress...';
|
||||
}
|
||||
|
||||
if (taskTimer) {
|
||||
taskTimerContent = taskTimer.textContent || '';
|
||||
}
|
||||
|
||||
// Update webcam overlay with task information
|
||||
if (overlayTaskType) {
|
||||
console.log('🎬 Task title content:', taskTitleText);
|
||||
if (taskTitleText.toLowerCase().includes('consequence')) {
|
||||
overlayTaskType.textContent = 'CONSEQUENCE';
|
||||
overlayTaskType.style.color = '#ff4757';
|
||||
console.log('🎬 Set webcam overlay type to CONSEQUENCE');
|
||||
} else if (taskTitleText.toLowerCase().includes('task')) {
|
||||
overlayTaskType.textContent = 'TASK';
|
||||
overlayTaskType.style.color = '#00d4ff';
|
||||
console.log('🎬 Set webcam overlay type to TASK');
|
||||
} else if (taskTitleText) {
|
||||
overlayTaskType.textContent = 'SESSION';
|
||||
overlayTaskType.style.color = '#ffd700';
|
||||
console.log('🎬 Set webcam overlay type to SESSION');
|
||||
} else {
|
||||
overlayTaskType.textContent = 'TRAINING';
|
||||
overlayTaskType.style.color = '#00ff00';
|
||||
console.log('🎬 Set webcam overlay type to TRAINING (fallback)');
|
||||
}
|
||||
}
|
||||
|
||||
if (overlayTaskText) {
|
||||
const text = taskTextContent || taskTitleText || 'Training session in progress...';
|
||||
console.log('🎬 Updating webcam overlay text to:', text);
|
||||
overlayTaskText.textContent = text;
|
||||
}
|
||||
|
||||
if (overlayTimer) {
|
||||
console.log('🎬 Task timer content:', taskTimerContent);
|
||||
if (taskTimerContent && taskTimerContent !== '0:00' && taskTimerContent !== '') {
|
||||
overlayTimer.textContent = taskTimerContent;
|
||||
overlayTimer.style.display = 'inline';
|
||||
console.log('🎬 Showing timer in webcam overlay:', taskTimerContent);
|
||||
} else {
|
||||
overlayTimer.textContent = '00:00';
|
||||
overlayTimer.style.display = 'inline';
|
||||
console.log('🎬 Set default timer in webcam overlay');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call this function whenever task content changes
|
||||
function syncTaskWithOverlay() {
|
||||
console.log('🎬 syncTaskWithOverlay called, recordingOverlayEnabled:', recordingOverlayEnabled);
|
||||
if (recordingOverlayEnabled) {
|
||||
updateRecordingOverlay();
|
||||
|
||||
// Also update webcam overlay elements directly for immediate display
|
||||
updateWebcamOverlayElements();
|
||||
}
|
||||
}
|
||||
|
||||
function updateWebcamOverlayElements() {
|
||||
const taskTitleElement = document.getElementById('task-title');
|
||||
const webcamTaskText = document.getElementById('webcam-task-text');
|
||||
const webcamTaskType = document.getElementById('webcam-task-type');
|
||||
|
||||
if (taskTitleElement && webcamTaskText && taskTitleElement.textContent.trim()) {
|
||||
webcamTaskText.textContent = taskTitleElement.textContent.trim();
|
||||
console.log('🎬 Updated webcam overlay with actual task:', taskTitleElement.textContent.trim());
|
||||
}
|
||||
|
||||
if (webcamTaskType) {
|
||||
const title = taskTitleElement?.textContent || '';
|
||||
if (title.toLowerCase().includes('consequence')) {
|
||||
webcamTaskType.textContent = 'CONSEQUENCE';
|
||||
webcamTaskType.style.color = '#ff4757';
|
||||
} else if (title.toLowerCase().includes('task')) {
|
||||
webcamTaskType.textContent = 'TASK';
|
||||
webcamTaskType.style.color = '#00d4ff';
|
||||
} else if (title) {
|
||||
webcamTaskType.textContent = 'SESSION';
|
||||
webcamTaskType.style.color = '#ffd700';
|
||||
} else {
|
||||
webcamTaskType.textContent = 'TRAINING';
|
||||
webcamTaskType.style.color = '#00ff00';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
console.log('Setting up event listeners...');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Navigation buttons with error handling
|
||||
const backToHomeBtn = document.getElementById('back-to-home');
|
||||
if (backToHomeBtn) {
|
||||
|
|
@ -3414,6 +3644,9 @@
|
|||
taskText.style.display = 'none'; // Hide the description for consequence tasks
|
||||
}
|
||||
|
||||
// Update recording overlay with new task
|
||||
syncTaskWithOverlay();
|
||||
|
||||
// Hide skip button for consequence tasks
|
||||
if (skipBtn) {
|
||||
skipBtn.style.display = 'none';
|
||||
|
|
@ -3551,6 +3784,9 @@
|
|||
};
|
||||
}
|
||||
|
||||
// Update recording overlay with new task
|
||||
syncTaskWithOverlay();
|
||||
|
||||
// Set up button event handlers with proper error handling
|
||||
if (completeBtn) {
|
||||
completeBtn.style.display = 'inline-block';
|
||||
|
|
@ -3725,6 +3961,9 @@
|
|||
} else {
|
||||
timerElement.style.color = '#00d4ff'; // Blue normally
|
||||
}
|
||||
|
||||
// Update recording overlay when timer changes
|
||||
syncTaskWithOverlay();
|
||||
}
|
||||
|
||||
function stopTaskTimer() {
|
||||
|
|
@ -4122,6 +4361,9 @@
|
|||
taskText.textContent = 'Your task will appear here';
|
||||
}
|
||||
}
|
||||
|
||||
// Update recording overlay whenever task display changes
|
||||
syncTaskWithOverlay();
|
||||
}
|
||||
|
||||
function updateGameStatus(gameState) {
|
||||
|
|
@ -4536,6 +4778,10 @@
|
|||
let mediaRecorder = null;
|
||||
let recordedChunks = [];
|
||||
let webcamStream = null;
|
||||
let recordingCanvas = null;
|
||||
let recordingCanvasContext = null;
|
||||
let compositeStream = null;
|
||||
let animationFrameId = null;
|
||||
|
||||
async function initializeWebcamRecording() {
|
||||
console.log('🎥 Initializing webcam recording for session...');
|
||||
|
|
@ -4559,8 +4805,8 @@
|
|||
|
||||
webcamPreview.srcObject = webcamStream;
|
||||
|
||||
// Apply user settings
|
||||
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
|
||||
// Apply user settings and enable recording overlay by default
|
||||
webcamViewer.className = `webcam-viewer active recording ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
|
||||
|
||||
// Set up recording - Force MP4 format for Electron
|
||||
recordedChunks = [];
|
||||
|
|
@ -4575,18 +4821,41 @@
|
|||
|
||||
let mediaRecorderCreated = false;
|
||||
|
||||
// Set up canvas for compositing video with overlays
|
||||
recordingCanvas = document.createElement('canvas');
|
||||
recordingCanvas.width = 1920;
|
||||
recordingCanvas.height = 1080;
|
||||
recordingCanvasContext = recordingCanvas.getContext('2d');
|
||||
|
||||
// Create composite stream from canvas
|
||||
compositeStream = recordingCanvas.captureStream(30); // 30 FPS
|
||||
|
||||
// Start compositing loop and recording timer
|
||||
startVideoCompositing();
|
||||
|
||||
// Start recording timer for overlay
|
||||
if (!recordingTimerInterval) {
|
||||
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
|
||||
}
|
||||
|
||||
// Initialize overlay with current task content
|
||||
setTimeout(() => {
|
||||
updateRecordingOverlay();
|
||||
updateWebcamOverlayElements();
|
||||
}, 1000);
|
||||
|
||||
// Try each MP4 codec until one works
|
||||
for (const codec of mp4Codecs) {
|
||||
if (MediaRecorder.isTypeSupported(codec)) {
|
||||
try {
|
||||
mediaRecorder = new MediaRecorder(webcamStream, {
|
||||
mediaRecorder = new MediaRecorder(compositeStream, {
|
||||
mimeType: codec,
|
||||
videoBitsPerSecond: 4000000, // 4 Mbps for high quality 1080p
|
||||
bitsPerSecond: 4000000 // Fallback for older browsers
|
||||
});
|
||||
quickPlaySettings.recordingMimeType = codec;
|
||||
quickPlaySettings.recordingExtension = 'mp4';
|
||||
console.log('🎥 Successfully using MP4 codec:', codec);
|
||||
console.log('🎥 Successfully using MP4 codec with compositing:', codec);
|
||||
mediaRecorderCreated = true;
|
||||
break;
|
||||
} catch (error) {
|
||||
|
|
@ -4690,9 +4959,141 @@
|
|||
});
|
||||
}
|
||||
|
||||
function startVideoCompositing() {
|
||||
console.log('🎨 Starting video compositing with task overlay...');
|
||||
|
||||
function drawFrame() {
|
||||
if (!recordingCanvas || !recordingCanvasContext || !webcamStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webcamPreview = document.getElementById('webcam-preview');
|
||||
if (!webcamPreview || webcamPreview.videoWidth === 0) {
|
||||
animationFrameId = requestAnimationFrame(drawFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear canvas
|
||||
recordingCanvasContext.clearRect(0, 0, recordingCanvas.width, recordingCanvas.height);
|
||||
|
||||
// Draw webcam video
|
||||
recordingCanvasContext.drawImage(
|
||||
webcamPreview,
|
||||
0, 0,
|
||||
recordingCanvas.width,
|
||||
recordingCanvas.height
|
||||
);
|
||||
|
||||
// Draw task overlay if enabled
|
||||
if (recordingOverlayEnabled) {
|
||||
drawTaskOverlayOnCanvas();
|
||||
}
|
||||
|
||||
// Continue loop
|
||||
animationFrameId = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
|
||||
drawFrame();
|
||||
}
|
||||
|
||||
function drawTaskOverlayOnCanvas() {
|
||||
const ctx = recordingCanvasContext;
|
||||
if (!ctx) return;
|
||||
|
||||
// Get current task information - prioritize actual task title over generic description
|
||||
const taskTitleElement = document.getElementById('task-title');
|
||||
const taskTextElement = document.getElementById('task-text');
|
||||
|
||||
let actualTaskText = '';
|
||||
if (taskTitleElement && taskTitleElement.textContent && taskTitleElement.textContent.trim()) {
|
||||
actualTaskText = taskTitleElement.textContent.trim();
|
||||
} else if (taskTextElement && taskTextElement.textContent && taskTextElement.textContent.trim()) {
|
||||
actualTaskText = taskTextElement.textContent.trim();
|
||||
} else {
|
||||
actualTaskText = 'Training session in progress...';
|
||||
}
|
||||
|
||||
const taskType = document.getElementById('webcam-task-type')?.textContent || 'SESSION';
|
||||
const taskTimer = document.getElementById('webcam-task-timer')?.textContent || '00:00';
|
||||
|
||||
// Set up text styling
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Calculate overlay position (bottom area)
|
||||
const overlayHeight = 120;
|
||||
const overlayY = recordingCanvas.height - overlayHeight - 20;
|
||||
const overlayX = 20;
|
||||
const overlayWidth = recordingCanvas.width - 40;
|
||||
|
||||
// Draw overlay background
|
||||
ctx.fillRect(overlayX, overlayY, overlayWidth, overlayHeight);
|
||||
ctx.strokeRect(overlayX, overlayY, overlayWidth, overlayHeight);
|
||||
|
||||
// Draw task type and timer header
|
||||
ctx.fillStyle = '#00ff00';
|
||||
ctx.font = 'bold 28px Arial';
|
||||
ctx.fillText(taskType, overlayX + 15, overlayY + 35);
|
||||
|
||||
ctx.fillStyle = '#ffa500';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
const timerWidth = ctx.measureText(taskTimer).width;
|
||||
ctx.fillText(taskTimer, overlayX + overlayWidth - timerWidth - 15, overlayY + 35);
|
||||
|
||||
// Draw task text (with word wrapping)
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '22px Arial';
|
||||
|
||||
const maxWidth = overlayWidth - 30;
|
||||
const lineHeight = 30;
|
||||
const words = actualTaskText.split(' ');
|
||||
let line = '';
|
||||
let y = overlayY + 70;
|
||||
|
||||
for (let n = 0; n < words.length; n++) {
|
||||
const testLine = line + words[n] + ' ';
|
||||
const metrics = ctx.measureText(testLine);
|
||||
const testWidth = metrics.width;
|
||||
|
||||
if (testWidth > maxWidth && n > 0) {
|
||||
ctx.fillText(line, overlayX + 15, y);
|
||||
line = words[n] + ' ';
|
||||
y += lineHeight;
|
||||
|
||||
// Prevent overflow
|
||||
if (y > overlayY + overlayHeight - 10) break;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
ctx.fillText(line, overlayX + 15, y);
|
||||
}
|
||||
|
||||
function stopVideoCompositing() {
|
||||
console.log('⏹️ Stopping video compositing...');
|
||||
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
|
||||
if (compositeStream) {
|
||||
const tracks = compositeStream.getTracks();
|
||||
tracks.forEach(track => track.stop());
|
||||
compositeStream = null;
|
||||
}
|
||||
|
||||
recordingCanvas = null;
|
||||
recordingCanvasContext = null;
|
||||
}
|
||||
|
||||
function stopWebcamRecording() {
|
||||
console.log('⏹️ Stopping webcam recording...');
|
||||
|
||||
// Stop video compositing
|
||||
stopVideoCompositing();
|
||||
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
|
|
@ -7086,10 +7487,49 @@
|
|||
background: linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #0f0f23 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Electrolize', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.recording-overlay-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recording-overlay-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.recording-timestamp {
|
||||
color: #ffd700;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.quick-play-header {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-bottom: 1px solid #00d4ff;
|
||||
|
|
@ -9737,12 +10177,78 @@
|
|||
top: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
/* Webcam Task Overlay Styles */
|
||||
.webcam-task-overlay {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid #00ff00;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.webcam-task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.webcam-task-overlay #webcam-task-type {
|
||||
color: #00ff00;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.webcam-task-overlay #webcam-task-timer {
|
||||
color: #ffa500;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.webcam-task-overlay #webcam-task-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
|
||||
word-wrap: break-word;
|
||||
max-height: 3em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide webcam task overlay when not recording */
|
||||
.webcam-viewer:not(.recording) .webcam-task-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show webcam task overlay when recording */
|
||||
.webcam-viewer.recording .webcam-task-overlay {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Floating Webcam Viewer -->
|
||||
<div id="webcam-viewer" class="webcam-viewer">
|
||||
<div class="recording-indicator">● REC</div>
|
||||
<video id="webcam-preview" autoplay muted></video>
|
||||
|
||||
<!-- Task Overlay on Webcam Recording -->
|
||||
<div id="webcam-task-overlay" class="webcam-task-overlay">
|
||||
<div class="webcam-task-header">
|
||||
<span id="webcam-task-type">SESSION</span>
|
||||
<span id="webcam-task-timer">00:00</span>
|
||||
</div>
|
||||
<div id="webcam-task-text">Training session active...</div>
|
||||
</div>
|
||||
|
||||
<div class="viewer-controls">
|
||||
<button class="control-btn" id="toggle-webcam-viewer" title="Hide Webcam">👁️</button>
|
||||
<button class="control-btn" id="stop-recording" title="Stop Recording">⏹️</button>
|
||||
|
|
|
|||
|
|
@ -1147,12 +1147,197 @@ class WebcamManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bank of verification positions for randomized instructions
|
||||
*/
|
||||
getPositionBank() {
|
||||
return {
|
||||
submissive: [
|
||||
{
|
||||
name: "Kneeling Submission",
|
||||
instruction: "Kneel with hands behind your back, head bowed in submission",
|
||||
description: "Classic submissive kneeling pose showing complete obedience"
|
||||
},
|
||||
{
|
||||
name: "Present Position",
|
||||
instruction: "On hands and knees, arch your back and present yourself for inspection",
|
||||
description: "Degrading presentation pose for thorough examination"
|
||||
},
|
||||
{
|
||||
name: "Worship Pose",
|
||||
instruction: "Sit on your heels with hands on thighs, mouth open in worship position",
|
||||
description: "Ready to worship and serve in proper position"
|
||||
},
|
||||
{
|
||||
name: "Display Spread",
|
||||
instruction: "Lying back with legs spread wide, hands above head in surrender",
|
||||
description: "Complete exposure and vulnerability display"
|
||||
},
|
||||
{
|
||||
name: "Begging Position",
|
||||
instruction: "On knees with hands clasped together, looking up desperately",
|
||||
description: "Pathetic begging pose showing your desperate need"
|
||||
}
|
||||
],
|
||||
degrading: [
|
||||
{
|
||||
name: "Pet Crawl",
|
||||
instruction: "On all fours like an animal, tongue out panting like a dog",
|
||||
description: "Dehumanizing animal position for complete degradation"
|
||||
},
|
||||
{
|
||||
name: "Toilet Position",
|
||||
instruction: "Squatting low with mouth open wide, ready to be used",
|
||||
description: "Ultimate degradation pose as a human toilet"
|
||||
},
|
||||
{
|
||||
name: "Fuck-Doll Display",
|
||||
instruction: "Lying with limbs spread, completely limp like a broken sex doll",
|
||||
description: "Objectified as nothing but a mindless fuck toy"
|
||||
},
|
||||
{
|
||||
name: "Cock-Socket Ready",
|
||||
instruction: "Kneeling with mouth wide open, hands holding face open",
|
||||
description: "Prepared to be nothing but a hole for use"
|
||||
},
|
||||
{
|
||||
name: "Pain Slut Pose",
|
||||
instruction: "Bent over with back arched, presenting for punishment",
|
||||
description: "Ready to receive pain and discipline like the slut you are"
|
||||
}
|
||||
],
|
||||
humiliating: [
|
||||
{
|
||||
name: "Embarrassed Display",
|
||||
instruction: "Standing with hands covering face in shame while body exposed",
|
||||
description: "Showing your humiliation and embarrassment"
|
||||
},
|
||||
{
|
||||
name: "Pathetic Grovel",
|
||||
instruction: "Face down on ground, ass up in the air, groveling pathetically",
|
||||
description: "Ultimate humiliation and subservience position"
|
||||
},
|
||||
{
|
||||
name: "Shame Spread",
|
||||
instruction: "Squatting with legs wide, hands pulling yourself open",
|
||||
description: "Shameful self-exposure for maximum humiliation"
|
||||
},
|
||||
{
|
||||
name: "Desperate Beg",
|
||||
instruction: "On back with legs in the air, begging desperately for attention",
|
||||
description: "Pathetic attention-seeking humiliation pose"
|
||||
},
|
||||
{
|
||||
name: "Slut Presentation",
|
||||
instruction: "Bent forward touching toes, looking back with slutty expression",
|
||||
description: "Presenting yourself like the desperate slut you are"
|
||||
}
|
||||
],
|
||||
extreme: [
|
||||
{
|
||||
name: "Complete Surrender",
|
||||
instruction: "Spread eagle on back, completely exposed and helpless",
|
||||
description: "Total vulnerability and surrender to be used"
|
||||
},
|
||||
{
|
||||
name: "Breeding Position",
|
||||
instruction: "Face down ass up, legs spread wide for breeding access",
|
||||
description: "Perfect position for being bred like an animal"
|
||||
},
|
||||
{
|
||||
name: "Inspection Pose",
|
||||
instruction: "Squatting with hands spreading yourself open for inspection",
|
||||
description: "Allowing complete inspection of your worthless holes"
|
||||
},
|
||||
{
|
||||
name: "Punishment Ready",
|
||||
instruction: "Bent over grabbing ankles, presenting for harsh punishment",
|
||||
description: "Ready to receive the brutal punishment you deserve"
|
||||
},
|
||||
{
|
||||
name: "Broken Toy",
|
||||
instruction: "Collapsed in heap, limbs akimbo like a discarded fuck toy",
|
||||
description: "Completely broken and used up, nothing left but a toy"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Select random position from bank based on intensity
|
||||
*/
|
||||
selectRandomPosition(intensity = 'mixed') {
|
||||
const positions = this.getPositionBank();
|
||||
|
||||
let availablePositions = [];
|
||||
|
||||
if (intensity === 'mixed') {
|
||||
// Mix all categories
|
||||
availablePositions = [
|
||||
...positions.submissive,
|
||||
...positions.degrading,
|
||||
...positions.humiliating,
|
||||
...positions.extreme
|
||||
];
|
||||
} else if (positions[intensity]) {
|
||||
availablePositions = positions[intensity];
|
||||
} else {
|
||||
// Default to submissive if invalid intensity
|
||||
availablePositions = positions.submissive;
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * availablePositions.length);
|
||||
return availablePositions[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up localStorage to prevent quota exceeded errors
|
||||
*/
|
||||
cleanupStorageBeforeVerification() {
|
||||
try {
|
||||
// Clean up old verification photos
|
||||
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
if (verificationPhotos.length > 3) {
|
||||
const recent = verificationPhotos.slice(-3);
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Cleaned up verification photos: ${verificationPhotos.length} → ${recent.length}`);
|
||||
}
|
||||
|
||||
// Clean up old captured photos
|
||||
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
if (capturedPhotos.length > 5) {
|
||||
const recent = capturedPhotos.slice(-5);
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Cleaned up captured photos: ${capturedPhotos.length} → ${recent.length}`);
|
||||
}
|
||||
|
||||
// Clean up any old individual photo keys
|
||||
const keys = Object.keys(localStorage);
|
||||
const photoKeys = keys.filter(key => key.startsWith('verificationPhoto_'));
|
||||
if (photoKeys.length > 10) {
|
||||
photoKeys.slice(0, -10).forEach(key => {
|
||||
const url = localStorage.getItem(key);
|
||||
if (url && url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
console.log(`🧹 Cleaned up ${photoKeys.length - 10} old photo keys`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Error during storage cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start webcam verification mode for position verification
|
||||
*/
|
||||
async startVerificationMode(verificationData) {
|
||||
console.log('🔍 Starting webcam verification mode');
|
||||
|
||||
// Clean up storage before starting to prevent quota issues
|
||||
this.cleanupStorageBeforeVerification();
|
||||
|
||||
// Request camera if not already active
|
||||
if (!this.isActive) {
|
||||
const accessGranted = await this.requestCameraAccess();
|
||||
|
|
@ -1176,6 +1361,12 @@ class WebcamManager {
|
|||
* Display verification interface - webcam feed with position verification
|
||||
*/
|
||||
showVerificationInterface(verificationData) {
|
||||
// Select random position for this verification
|
||||
const selectedPosition = this.selectRandomPosition(verificationData?.positionIntensity || 'mixed');
|
||||
|
||||
// Store selected position for use throughout verification
|
||||
this.currentPosition = selectedPosition;
|
||||
|
||||
// Create verification overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'verification-overlay';
|
||||
|
|
@ -1188,8 +1379,14 @@ class WebcamManager {
|
|||
<div class="verification-video-container">
|
||||
<video id="verification-video" autoplay muted playsinline></video>
|
||||
<div class="verification-overlay-text">
|
||||
<div class="position-name" style="color: #ff6b6b; font-weight: bold; font-size: 1.2em; margin-bottom: 10px;">
|
||||
${selectedPosition.name}
|
||||
</div>
|
||||
<div class="position-instructions">
|
||||
${verificationData?.verificationInstructions || 'Assume required position'}
|
||||
${selectedPosition.instruction}
|
||||
</div>
|
||||
<div class="position-description" style="color: #ffd700; font-style: italic; font-size: 0.9em; margin-top: 10px;">
|
||||
${selectedPosition.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1198,7 +1395,7 @@ class WebcamManager {
|
|||
<div class="progress-bar" id="verification-progress-bar"></div>
|
||||
</div>
|
||||
<div class="timer-text">Verification time: <span id="verification-time">${verificationData?.verificationDuration || 30}</span>s</div>
|
||||
<div class="verification-status" id="verification-status">Get in position...</div>
|
||||
<div class="verification-status" id="verification-status">Prepare for position verification...</div>
|
||||
</div>
|
||||
<div class="verification-controls">
|
||||
<button id="verification-start-btn" class="btn btn-primary">
|
||||
|
|
@ -1207,6 +1404,9 @@ class WebcamManager {
|
|||
<button id="verification-close-btn" class="btn btn-secondary">
|
||||
❌ Abandon Task
|
||||
</button>
|
||||
<button id="verification-force-close-btn" class="btn btn-danger" style="background: #dc3545; margin-left: 10px;">
|
||||
🚨 Force Close
|
||||
</button>
|
||||
</div>
|
||||
${verificationData?.verificationText ? `<div class="verification-task-text">${verificationData.verificationText}</div>` : ''}
|
||||
</div>
|
||||
|
|
@ -1300,6 +1500,29 @@ class WebcamManager {
|
|||
this.showCondescendingGameOver();
|
||||
});
|
||||
|
||||
// Add force close button handler
|
||||
const forceCloseBtn = overlay.querySelector('#verification-force-close-btn');
|
||||
if (forceCloseBtn) {
|
||||
forceCloseBtn.addEventListener('click', () => {
|
||||
console.log('🚨 Force closing verification mode');
|
||||
this.closeVerificationMode();
|
||||
// Don't trigger game over, just close
|
||||
});
|
||||
}
|
||||
|
||||
// Add escape key handler for emergency close
|
||||
const escapeHandler = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('🚨 Emergency closing verification with Escape key');
|
||||
document.removeEventListener('keydown', escapeHandler);
|
||||
this.closeVerificationMode();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escapeHandler);
|
||||
|
||||
// Store escape handler reference for cleanup
|
||||
overlay.escapeHandler = escapeHandler;
|
||||
|
||||
console.log('🔍 Verification interface displayed');
|
||||
}
|
||||
|
||||
|
|
@ -1330,32 +1553,62 @@ class WebcamManager {
|
|||
|
||||
// Preparation phase (10 seconds)
|
||||
let prepTime = 10;
|
||||
if (statusElement) statusElement.textContent = `Get in position now! ${prepTime}s`;
|
||||
const positionName = this.currentPosition ? this.currentPosition.name : 'required position';
|
||||
if (statusElement) statusElement.textContent = `Get into ${positionName}! ${prepTime}s`;
|
||||
|
||||
const prepTimer = setInterval(() => {
|
||||
prepTime--;
|
||||
if (statusElement) statusElement.textContent = `Get in position now! ${prepTime}s`;
|
||||
|
||||
if (prepTime <= 0) {
|
||||
try {
|
||||
prepTime--;
|
||||
console.log('📍 Preparation countdown:', prepTime);
|
||||
|
||||
if (statusElement) statusElement.textContent = `Get into ${positionName}! ${prepTime}s`;
|
||||
|
||||
if (prepTime <= 0) {
|
||||
console.log('📍 Preparation complete, starting main verification...');
|
||||
clearInterval(prepTimer);
|
||||
this.preparationTimer = null;
|
||||
this.startMainVerification(duration, overlay);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error in preparation timer:', error);
|
||||
clearInterval(prepTimer);
|
||||
this.preparationTimer = null;
|
||||
// Emergency fallback: proceed to main verification anyway
|
||||
this.startMainVerification(duration, overlay);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Store timer reference for cleanup
|
||||
this.preparationTimer = prepTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start main verification phase with position holding
|
||||
*/
|
||||
startMainVerification(duration, overlay) {
|
||||
console.log('🔍 Starting main verification phase with duration:', duration);
|
||||
|
||||
const timerDisplay = overlay.querySelector('#verification-time');
|
||||
const progressBar = overlay.querySelector('#verification-progress-bar');
|
||||
const statusElement = overlay.querySelector('#verification-status');
|
||||
|
||||
if (!overlay || !overlay.parentNode) {
|
||||
console.error('❌ Overlay no longer exists, aborting verification');
|
||||
return;
|
||||
}
|
||||
|
||||
// Take START photo now that they're in position and verification begins
|
||||
this.capturePositionPhoto('start', overlay, () => {
|
||||
// Continue with verification after START photo
|
||||
try {
|
||||
this.capturePositionPhoto('start', overlay, () => {
|
||||
// Continue with verification after START photo
|
||||
console.log('📸 START photo captured, continuing verification...');
|
||||
this.continueMainVerification(duration, overlay);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error capturing start photo:', error);
|
||||
// Continue anyway without photo
|
||||
this.continueMainVerification(duration, overlay);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1416,10 +1669,21 @@ class WebcamManager {
|
|||
|
||||
// Capture final position verification photo with degrading message
|
||||
this.capturePositionPhoto('end', overlay, () => {
|
||||
// Finalize verification after final photo
|
||||
console.log('📸 Final photo captured, finalizing verification...');
|
||||
// Reduced delay and added error handling
|
||||
setTimeout(() => {
|
||||
this.finalizeVerification(overlay);
|
||||
}, 2000);
|
||||
try {
|
||||
this.finalizeVerification(overlay);
|
||||
} catch (error) {
|
||||
console.error('❌ Error during finalization:', error);
|
||||
// Force close even if there's an error
|
||||
this.closeVerificationMode();
|
||||
const event = new CustomEvent('verificationComplete', {
|
||||
detail: { success: true }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1444,51 +1708,91 @@ class WebcamManager {
|
|||
console.log('📸 Video ready:', video && video.videoWidth > 0);
|
||||
console.log('📸 Video ID:', video?.id);
|
||||
|
||||
// Array of degrading messages for different phases
|
||||
// Array of brutally degrading messages for different phases
|
||||
const degradingMessages = {
|
||||
start: [
|
||||
"📸 *SNAP* - Look at you, ready to humiliate yourself for training...",
|
||||
"📸 *SNAP* - Another pathetic position documented. How degrading!",
|
||||
"📸 *SNAP* - Smile for your shame! This is just the beginning...",
|
||||
"📸 *SNAP* - Perfect. Now everyone can see how eager you are to submit.",
|
||||
"📸 *SNAP* - Such a good little trainee, posing on command like that.",
|
||||
"📸 *SNAP* - Look at yourself... so desperate for approval and training.",
|
||||
"📸 *SNAP* - Starting position captured! Time to show your dedication...",
|
||||
"📸 *SNAP* - What a good little gooner, already in position for training!",
|
||||
"📸 *SNAP* - Beginning documentation of your descent into pleasure...",
|
||||
"📸 *SNAP* - There we go! Ready to be molded into the perfect toy...",
|
||||
"📸 *SNAP* - Such an eager student, posing so willingly for the camera!",
|
||||
"📸 *SNAP* - Initial submission documented. You look so vulnerable...",
|
||||
"📸 *SNAP* - Starting your journey to complete obedience. Smile!",
|
||||
"📸 *SNAP* - Look how excited you are to begin your training session!",
|
||||
"📸 *SNAP* - Perfect positioning! You're learning to follow orders well...",
|
||||
"📸 *SNAP* - Pre-training photo captured. You look so innocent... for now.",
|
||||
"📸 *SNAP* - Ready to begin your transformation? This photo says yes!",
|
||||
"📸 *SNAP* - Starting pose documented. Let's see how far you'll go...",
|
||||
"📸 *SNAP* - Initial compliance captured! Time to push your limits...",
|
||||
"📸 *SNAP* - Beginning your session with such enthusiasm! How cute..."
|
||||
"📸 *SNAP* - Look at this pathetic slut, already desperate to be used...",
|
||||
"📸 *SNAP* - What a worthless whore, posing like the cum-hungry toy you are!",
|
||||
"📸 *SNAP* - Smile for daddy, you filthy little cock sleeve!",
|
||||
"📸 *SNAP* - Perfect positioning, you brain-dead fuck doll. Ready to be broken?",
|
||||
"📸 *SNAP* - Such an eager little cum dump, already wet and waiting!",
|
||||
"📸 *SNAP* - Look at yourself, you depraved slut... begging to be degraded!",
|
||||
"📸 *SNAP* - Starting position: worthless whore ready for brutal training!",
|
||||
"📸 *SNAP* - What a mindless gooning slut! Already dripping for abuse...",
|
||||
"📸 *SNAP* - Beginning your transformation into a brainless fuck toy!",
|
||||
"📸 *SNAP* - There's my good little pain slut, ready to be destroyed!",
|
||||
"📸 *SNAP* - Such a desperate cock-hungry whore, posing like the slut you are!",
|
||||
"📸 *SNAP* - Initial degradation documented. You look so fucking pathetic...",
|
||||
"📸 *SNAP* - Starting your descent into complete cock slavery!",
|
||||
"📸 *SNAP* - Look how excited this filthy slut is to be humiliated!",
|
||||
"📸 *SNAP* - Perfect! My little cum bucket is ready for brutal conditioning!",
|
||||
"📸 *SNAP* - Pre-training: one worthless whore about to be broken completely!",
|
||||
"📸 *SNAP* - Ready to become daddy's mindless fuck doll? This says yes!",
|
||||
"📸 *SNAP* - Starting pose: pathetic slut begging to be used and abused!",
|
||||
"📸 *SNAP* - Initial submission: my cock-sleeve ready for brutal training!",
|
||||
"📸 *SNAP* - Look at this eager little pain pig, so ready to suffer!",
|
||||
"📸 *SNAP* - Fresh meat ready for psychological destruction! How delicious...",
|
||||
"📸 *SNAP* - Another desperate whore volunteering for mind-rape! Perfect!",
|
||||
"📸 *SNAP* - Look at those needy eyes... already begging to be owned!",
|
||||
"📸 *SNAP* - What a disgusting little cum-addicted freak you are!",
|
||||
"📸 *SNAP* - Time to shatter this worthless slut's mind completely!",
|
||||
"📸 *SNAP* - Beginning the process of turning you into daddy's toilet!",
|
||||
"📸 *SNAP* - Such a pathetic cock-worshipping bitch, ready to be ruined!",
|
||||
"📸 *SNAP* - Starting documentation of your complete mental breakdown!",
|
||||
"📸 *SNAP* - What an obedient little fuck-meat, posing for degradation!",
|
||||
"📸 *SNAP* - Time to train this brain-dead slut into perfect submission!",
|
||||
"📸 *SNAP* - Look at this worthless cum-rag, so eager to be destroyed!",
|
||||
"📸 *SNAP* - Beginning your transformation from human to sex object!",
|
||||
"📸 *SNAP* - Such a desperate attention whore, begging to be humiliated!",
|
||||
"📸 *SNAP* - Ready to have your mind fucked beyond repair? Let's begin!",
|
||||
"📸 *SNAP* - What a filthy pain-slut, already assuming the position!",
|
||||
"📸 *SNAP* - Time to break this worthless toy and reprogram its brain!",
|
||||
"📸 *SNAP* - Look at this cock-starved whore, so ready for brutal training!",
|
||||
"📸 *SNAP* - Beginning the systematic destruction of your self-worth!",
|
||||
"📸 *SNAP* - Such an eager little degradation junkie, ready for abuse!",
|
||||
"📸 *SNAP* - Starting pose captured: one more slut ready for mind-fucking!"
|
||||
],
|
||||
end: [
|
||||
"📸 *SNAP* - Final documentation of your compliance. How obedient!",
|
||||
"📸 *SNAP* - There's the proof of your dedication to degradation.",
|
||||
"📸 *SNAP* - Perfect! Another successful training position documented.",
|
||||
"📸 *SNAP* - Look how well-trained you've become. Such a good sub.",
|
||||
"📸 *SNAP* - Evidence captured of your complete submission to training.",
|
||||
"📸 *SNAP* - Beautiful! You held that humiliating pose perfectly.",
|
||||
"📸 *SNAP* - Session complete! Look how thoroughly you've been trained...",
|
||||
"📸 *SNAP* - Final proof of your transformation into a perfect gooner!",
|
||||
"📸 *SNAP* - Post-training documentation shows your complete surrender...",
|
||||
"📸 *SNAP* - There's the evidence of how far you've fallen! Magnificent!",
|
||||
"📸 *SNAP* - Training complete! You've been so thoroughly conditioned...",
|
||||
"📸 *SNAP* - Final verification: another successful session of submission!",
|
||||
"📸 *SNAP* - Look at you now... completely molded to perfection!",
|
||||
"📸 *SNAP* - Session concluded with total compliance documented!",
|
||||
"📸 *SNAP* - Perfect! Your dedication to being trained is captured forever...",
|
||||
"📸 *SNAP* - End result documented. You've exceeded all expectations!",
|
||||
"📸 *SNAP* - Training session complete! Your obedience is beautiful...",
|
||||
"📸 *SNAP* - Final photo proves your complete transformation! Well done...",
|
||||
"📸 *SNAP* - Session finished! You've been such a good, compliant trainee...",
|
||||
"📸 *SNAP* - Conclusion captured! Your journey to submission is documented..."
|
||||
"📸 *SNAP* - Final documentation: completely broken whore, perfectly trained!",
|
||||
"📸 *SNAP* - There's proof you're nothing but a mindless cum receptacle!",
|
||||
"📸 *SNAP* - Perfect! Another worthless slut successfully mind-fucked!",
|
||||
"📸 *SNAP* - Look how well-broken you are now, my obedient cock toy!",
|
||||
"📸 *SNAP* - Evidence captured: one more brain-dead fuck doll created!",
|
||||
"📸 *SNAP* - Beautiful! You're now a perfectly trained pain slut!",
|
||||
"📸 *SNAP* - Session complete: worthless whore successfully conditioned!",
|
||||
"📸 *SNAP* - Final proof of your transformation into daddy's cum dump!",
|
||||
"📸 *SNAP* - Post-training: completely mind-broken slut, mission accomplished!",
|
||||
"📸 *SNAP* - There's how far you've fallen, you pathetic cock slave!",
|
||||
"📸 *SNAP* - Training complete! Another mindless gooning slut created!",
|
||||
"📸 *SNAP* - Final verification: worthless whore successfully brain-washed!",
|
||||
"📸 *SNAP* - Look at you now... nothing but daddy's obedient fuck toy!",
|
||||
"📸 *SNAP* - Session concluded: one more slut completely mind-fucked!",
|
||||
"📸 *SNAP* - Perfect! My cock sleeve is now permanently brain-damaged!",
|
||||
"📸 *SNAP* - End result: worthless cum dump, perfectly trained and broken!",
|
||||
"📸 *SNAP* - Training complete: my pain pig is now completely obedient!",
|
||||
"📸 *SNAP* - Final photo: proof of successful slut conditioning program!",
|
||||
"📸 *SNAP* - Session finished: another mind-broken whore for daddy's use!",
|
||||
"📸 *SNAP* - Conclusion: pathetic slut successfully turned into cock toy!",
|
||||
"📸 *SNAP* - Mission accomplished: another human reduced to fuck-meat!",
|
||||
"📸 *SNAP* - Final result: worthless toilet perfectly programmed for use!",
|
||||
"📸 *SNAP* - Conditioning complete: mind successfully shattered and rebuilt!",
|
||||
"📸 *SNAP* - Perfect destruction: another soul crushed into submission!",
|
||||
"📸 *SNAP* - End state documented: completely dehumanized cock-socket!",
|
||||
"📸 *SNAP* - Training success: pathetic worm transformed into sex object!",
|
||||
"📸 *SNAP* - Final capture: broken toy ready for permanent ownership!",
|
||||
"📸 *SNAP* - Session complete: mind successfully fucked beyond repair!",
|
||||
"📸 *SNAP* - Beautiful ending: worthless slut now daddy's property forever!",
|
||||
"📸 *SNAP* - Training finished: another human reduced to cum-receptacle!",
|
||||
"📸 *SNAP* - Perfect conclusion: mind-rape successful, slut created!",
|
||||
"📸 *SNAP* - Final documentation: psychological destruction 100% complete!",
|
||||
"📸 *SNAP* - End result: pathetic worm now exists only to serve cock!",
|
||||
"📸 *SNAP* - Mission success: another worthless whore properly broken!",
|
||||
"📸 *SNAP* - Training complete: brain successfully melted into mush!",
|
||||
"📸 *SNAP* - Final proof: transformation from person to fuck-object complete!",
|
||||
"📸 *SNAP* - Perfect finale: another mind completely owned and destroyed!",
|
||||
"📸 *SNAP* - Session concluded: worthless cum-rag properly conditioned!",
|
||||
"📸 *SNAP* - End state: pathetic slut now permanently brain-fucked!",
|
||||
"📸 *SNAP* - Final capture: successful creation of mindless sex-slave!"
|
||||
]
|
||||
};
|
||||
|
||||
|
|
@ -1525,19 +1829,41 @@ class WebcamManager {
|
|||
message: randomMessage
|
||||
};
|
||||
|
||||
// Save to localStorage (verification photos)
|
||||
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
existingPhotos.push(verificationPhoto);
|
||||
|
||||
// Limit to last 20 verification photos to save space
|
||||
if (existingPhotos.length > 20) {
|
||||
existingPhotos.splice(0, existingPhotos.length - 20);
|
||||
// Save to localStorage (verification photos) with quota error handling
|
||||
try {
|
||||
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
// Add position info to verification photo
|
||||
verificationPhoto.position = this.currentPosition ? {
|
||||
name: this.currentPosition.name,
|
||||
instruction: this.currentPosition.instruction,
|
||||
description: this.currentPosition.description
|
||||
} : null;
|
||||
existingPhotos.push(verificationPhoto);
|
||||
|
||||
// Limit to last 5 verification photos to save space (reduced from 20)
|
||||
if (existingPhotos.length > 5) {
|
||||
existingPhotos.splice(0, existingPhotos.length - 5);
|
||||
}
|
||||
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
|
||||
console.log(`✅ Saved verification photo, total: ${existingPhotos.length}`);
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
console.warn('⚠️ Storage quota exceeded, clearing verification photos and retrying...');
|
||||
try {
|
||||
localStorage.removeItem('verificationPhotos');
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify([verificationPhoto]));
|
||||
console.log('✅ Saved verification photo after emergency cleanup');
|
||||
} catch (retryError) {
|
||||
console.error('❌ Failed to save verification photo even after cleanup:', retryError);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Error saving verification photo:', storageError);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
|
||||
|
||||
// ALSO store in main captured photos system for gallery integration
|
||||
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
// Create photo data object outside try block to ensure scope
|
||||
const mainPhotoData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
data: photoData,
|
||||
|
|
@ -1546,18 +1872,40 @@ class WebcamManager {
|
|||
type: 'position_verification',
|
||||
phase: phase,
|
||||
message: randomMessage,
|
||||
filename: `verification_${phase}_${Date.now()}.jpg`
|
||||
filename: `verification_${phase}_${Date.now()}.jpg`,
|
||||
position: this.currentPosition ? {
|
||||
name: this.currentPosition.name,
|
||||
instruction: this.currentPosition.instruction,
|
||||
description: this.currentPosition.description
|
||||
} : null
|
||||
};
|
||||
|
||||
mainCapturedPhotos.push(mainPhotoData);
|
||||
|
||||
// Limit main photos to 50 total
|
||||
if (mainCapturedPhotos.length > 50) {
|
||||
mainCapturedPhotos.splice(0, mainCapturedPhotos.length - 50);
|
||||
try {
|
||||
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
mainCapturedPhotos.push(mainPhotoData);
|
||||
|
||||
// Limit main photos to 10 total (reduced from 50 to save space)
|
||||
if (mainCapturedPhotos.length > 10) {
|
||||
mainCapturedPhotos.splice(0, mainCapturedPhotos.length - 10);
|
||||
}
|
||||
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(mainCapturedPhotos));
|
||||
console.log(`✅ Saved to main photos, total: ${mainCapturedPhotos.length}`);
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
console.warn('⚠️ Main photos storage quota exceeded, clearing and retrying...');
|
||||
try {
|
||||
localStorage.removeItem('capturedPhotos');
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify([mainPhotoData]));
|
||||
console.log('✅ Saved main photo after emergency cleanup');
|
||||
} catch (retryError) {
|
||||
console.error('❌ Failed to save main photo even after cleanup:', retryError);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Error saving main photo:', storageError);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(mainCapturedPhotos));
|
||||
|
||||
// Add to webcam manager's capturedPhotos array if available
|
||||
if (this.capturedPhotos) {
|
||||
this.capturedPhotos.push(mainPhotoData);
|
||||
|
|
@ -1631,13 +1979,35 @@ class WebcamManager {
|
|||
const verificationPhoto = {
|
||||
timestamp: new Date().toISOString(),
|
||||
data: photoData,
|
||||
type: 'position_verification'
|
||||
type: 'position_verification',
|
||||
position: this.currentPosition ? {
|
||||
name: this.currentPosition.name,
|
||||
instruction: this.currentPosition.instruction,
|
||||
description: this.currentPosition.description
|
||||
} : null
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
existingPhotos.push(verificationPhoto);
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
|
||||
// Save to localStorage with error handling
|
||||
try {
|
||||
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
existingPhotos.push(verificationPhoto);
|
||||
|
||||
// Limit to 5 most recent photos to prevent storage overflow
|
||||
const limitedPhotos = existingPhotos.slice(-5);
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(limitedPhotos));
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
console.warn('⚠️ Storage quota exceeded for verification photos, clearing old data...');
|
||||
try {
|
||||
localStorage.removeItem('verificationPhotos');
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify([verificationPhoto]));
|
||||
} catch (retryError) {
|
||||
console.error('❌ Failed to save verification photo after cleanup:', retryError);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Error saving verification photo array:', storageError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📸 Verification photo captured and stored');
|
||||
|
||||
|
|
@ -1646,10 +2016,22 @@ class WebcamManager {
|
|||
statusElement.style.color = '#27ae60';
|
||||
}
|
||||
|
||||
// Complete verification after 2 seconds
|
||||
// Complete verification after 1 second (reduced delay)
|
||||
console.log('⏰ Starting verification completion timer...');
|
||||
setTimeout(() => {
|
||||
this.finalizeVerification(overlay);
|
||||
}, 2000);
|
||||
console.log('⏰ Verification timer fired, calling finalizeVerification...');
|
||||
try {
|
||||
this.finalizeVerification(overlay);
|
||||
} catch (error) {
|
||||
console.error('❌ Error in finalizeVerification:', error);
|
||||
// Force close the modal even if there's an error
|
||||
this.closeVerificationMode();
|
||||
const event = new CustomEvent('verificationComplete', {
|
||||
detail: { success: true }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
console.error('❌ Failed to capture verification photo');
|
||||
if (statusElement) {
|
||||
|
|
@ -1664,14 +2046,36 @@ class WebcamManager {
|
|||
*/
|
||||
finalizeVerification(overlay) {
|
||||
console.log('✅ Position verification completed successfully');
|
||||
console.log('🔄 Finalizing verification process...');
|
||||
|
||||
this.closeVerificationMode();
|
||||
|
||||
// Notify completion
|
||||
const event = new CustomEvent('verificationComplete', {
|
||||
detail: { success: true }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
try {
|
||||
console.log('🔚 Calling closeVerificationMode...');
|
||||
this.closeVerificationMode();
|
||||
console.log('✅ Verification mode closed successfully');
|
||||
|
||||
// Notify completion with enhanced logging
|
||||
console.log('📢 Dispatching verificationComplete event...');
|
||||
const event = new CustomEvent('verificationComplete', {
|
||||
detail: { success: true }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
console.log('✅ verificationComplete event dispatched successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during finalization:', error);
|
||||
// Try to clean up anyway
|
||||
try {
|
||||
this.closeVerificationMode();
|
||||
} catch (closeError) {
|
||||
console.error('❌ Error during emergency close:', closeError);
|
||||
}
|
||||
|
||||
// Dispatch event anyway
|
||||
const event = new CustomEvent('verificationComplete', {
|
||||
detail: { success: true, error: error.message }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1691,14 +2095,33 @@ class WebcamManager {
|
|||
this.game.trackWebcamVerification(false);
|
||||
}
|
||||
|
||||
// Remove verification overlay
|
||||
// Remove verification overlay - try multiple methods for safety
|
||||
const overlay = document.getElementById('verification-overlay');
|
||||
if (overlay) {
|
||||
console.log('🔚 Removing verification overlay...');
|
||||
|
||||
// Remove escape key handler if it exists
|
||||
if (overlay.escapeHandler) {
|
||||
document.removeEventListener('keydown', overlay.escapeHandler);
|
||||
console.log('🔚 Removed escape key handler');
|
||||
}
|
||||
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
// Additional safety: remove any stray verification overlays
|
||||
const allVerificationOverlays = document.querySelectorAll('[id*="verification"], [class*="verification"]');
|
||||
allVerificationOverlays.forEach(element => {
|
||||
if (element.style.position === 'fixed' || element.style.zIndex > 9000) {
|
||||
console.log('🔚 Removing stray verification element:', element.id || element.className);
|
||||
element.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Stop camera stream
|
||||
this.stopCamera();
|
||||
|
||||
console.log('✅ Verification mode closed completely');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2784,7 +3207,7 @@ class WebcamManager {
|
|||
}
|
||||
|
||||
// Add phase indicator at top
|
||||
const phaseText = phase === 'start' ? '🟢 START POSITION' : '🔴 END POSITION';
|
||||
const phaseText = phase === 'start' ? '🔥 SLUT TRAINING BEGINS' : '💀 MIND-FUCKED & BROKEN';
|
||||
const phaseY = 40;
|
||||
|
||||
// Draw phase indicator with outline
|
||||
|
|
|
|||
|
|
@ -24,10 +24,22 @@ class BackupManager {
|
|||
*/
|
||||
createBackup(isAutomatic = false) {
|
||||
try {
|
||||
// Check storage usage before backup
|
||||
const currentUsage = this.calculateStorageUsage();
|
||||
const maxStorage = 5 * 1024 * 1024; // 5MB typical localStorage limit
|
||||
|
||||
console.log(`📊 Current storage usage: ${(currentUsage / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
if (currentUsage > maxStorage * 0.7) { // If over 70% usage
|
||||
console.warn('⚠️ Storage usage high, performing preemptive cleanup...');
|
||||
this.performEmergencyCleanup();
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const backupData = this.gatherAllUserData();
|
||||
|
||||
const backup = {
|
||||
// Check backup size and clean if too large
|
||||
const testBackup = {
|
||||
timestamp,
|
||||
version: '1.0',
|
||||
isAutomatic,
|
||||
|
|
@ -38,12 +50,47 @@ class BackupManager {
|
|||
appVersion: this.getAppVersion()
|
||||
}
|
||||
};
|
||||
|
||||
const backupJson = JSON.stringify(testBackup);
|
||||
const backupSize = new Blob([backupJson]).size;
|
||||
const maxSize = 1 * 1024 * 1024; // Reduced to 1MB limit for safety
|
||||
|
||||
console.log(`📊 Backup size: ${(backupSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
// Always clean photo data from backups to prevent quota issues
|
||||
console.log('🧹 Proactively cleaning photo data from backup...');
|
||||
backupData.capturedPhotos = this.cleanLargePhotoData(backupData.capturedPhotos);
|
||||
backupData.photoGallery = this.cleanLargePhotoData(backupData.photoGallery);
|
||||
backupData.verificationPhotos = this.cleanLargePhotoData(backupData.verificationPhotos);
|
||||
|
||||
// Update backup after cleaning
|
||||
testBackup.data = backupData;
|
||||
testBackup.metadata.totalItems = this.countDataItems(backupData);
|
||||
testBackup.metadata.photosReduced = true;
|
||||
|
||||
// Recalculate size after cleaning
|
||||
const cleanedSize = new Blob([JSON.stringify(testBackup)]).size;
|
||||
console.log(`📊 Cleaned backup size: ${(cleanedSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
const backupKey = `${this.backupPrefix}${Date.now()}`;
|
||||
localStorage.setItem(backupKey, JSON.stringify(backup));
|
||||
|
||||
try {
|
||||
localStorage.setItem(backupKey, JSON.stringify(testBackup));
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
console.warn('⚠️ Storage quota exceeded, performing emergency cleanup...');
|
||||
this.performEmergencyCleanup();
|
||||
|
||||
// Try again with minimal backup
|
||||
const minimalBackup = this.createMinimalBackup(isAutomatic);
|
||||
localStorage.setItem(backupKey, JSON.stringify(minimalBackup));
|
||||
} else {
|
||||
throw storageError;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🛡️ ${isAutomatic ? 'Auto' : 'Manual'} backup created: ${backupKey}`);
|
||||
console.log(`📊 Backup contains ${backup.metadata.totalItems} data items`);
|
||||
console.log(`📊 Backup contains ${testBackup.metadata.totalItems} data items`);
|
||||
|
||||
return backupKey;
|
||||
} catch (error) {
|
||||
|
|
@ -51,6 +98,135 @@ class BackupManager {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean large photo data to reduce backup size
|
||||
*/
|
||||
cleanLargePhotoData(photoDataString) {
|
||||
if (!photoDataString) return photoDataString;
|
||||
|
||||
try {
|
||||
const photoData = JSON.parse(photoDataString);
|
||||
if (Array.isArray(photoData)) {
|
||||
// Keep only metadata, remove actual image data
|
||||
const cleanedData = photoData.map(photo => ({
|
||||
...photo,
|
||||
data: photo.data ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.data,
|
||||
dataUrl: photo.dataUrl ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.dataUrl
|
||||
}));
|
||||
return JSON.stringify(cleanedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Error cleaning photo data:', error);
|
||||
}
|
||||
|
||||
return photoDataString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create minimal backup with only essential data
|
||||
*/
|
||||
createMinimalBackup(isAutomatic = false) {
|
||||
const essentialData = {};
|
||||
|
||||
// Only include essential game data, no photos
|
||||
const webGameData = localStorage.getItem('webGame-data');
|
||||
if (webGameData) {
|
||||
essentialData.webGameData = JSON.parse(webGameData);
|
||||
}
|
||||
|
||||
essentialData.playerStats = localStorage.getItem('playerStats');
|
||||
essentialData.achievements = localStorage.getItem('achievements');
|
||||
essentialData.selectedTheme = localStorage.getItem('selectedTheme');
|
||||
essentialData.userPreferences = localStorage.getItem('userPreferences');
|
||||
essentialData.disabledTasks = localStorage.getItem('disabledTasks');
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
isAutomatic,
|
||||
data: essentialData,
|
||||
metadata: {
|
||||
totalItems: this.countDataItems(essentialData),
|
||||
userAgent: navigator.userAgent,
|
||||
appVersion: this.getAppVersion(),
|
||||
isMinimal: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emergency cleanup when storage quota is exceeded
|
||||
*/
|
||||
performEmergencyCleanup() {
|
||||
try {
|
||||
console.log('🚨 Starting emergency storage cleanup...');
|
||||
|
||||
// Clean up old backups first - keep only 1
|
||||
this.cleanupOldBackups(1);
|
||||
|
||||
// Clean up verification photos aggressively
|
||||
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
if (verificationPhotos.length > 2) {
|
||||
const recent = verificationPhotos.slice(-2);
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Emergency: Reduced verification photos to ${recent.length}`);
|
||||
}
|
||||
|
||||
// Clean up captured photos aggressively
|
||||
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
if (capturedPhotos.length > 3) {
|
||||
const recent = capturedPhotos.slice(-3);
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Emergency: Reduced captured photos to ${recent.length}`);
|
||||
}
|
||||
|
||||
// Clean up any old individual photo keys
|
||||
const keys = Object.keys(localStorage);
|
||||
const photoKeys = keys.filter(key =>
|
||||
key.startsWith('verificationPhoto_') ||
|
||||
key.startsWith('capturedPhoto_') ||
|
||||
key.startsWith('photo_')
|
||||
);
|
||||
|
||||
if (photoKeys.length > 5) {
|
||||
const toRemove = photoKeys.slice(0, -5); // Keep only 5 most recent
|
||||
toRemove.forEach(key => {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value && value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(value);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
console.log(`🧹 Emergency: Removed ${toRemove.length} old photo keys`);
|
||||
}
|
||||
|
||||
// Calculate storage usage after cleanup
|
||||
const usage = this.calculateStorageUsage();
|
||||
console.log(`🧹 Emergency cleanup complete. Storage usage: ${(usage / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Emergency cleanup failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate approximate localStorage usage
|
||||
*/
|
||||
calculateStorageUsage() {
|
||||
let totalSize = 0;
|
||||
try {
|
||||
for (let key in localStorage) {
|
||||
if (localStorage.hasOwnProperty(key)) {
|
||||
const value = localStorage.getItem(key);
|
||||
totalSize += new Blob([key + value]).size;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not calculate storage usage:', error);
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather all user data from various storage systems
|
||||
|
|
@ -81,9 +257,10 @@ class BackupManager {
|
|||
data.achievements = localStorage.getItem('achievements');
|
||||
data.levelProgress = localStorage.getItem('levelProgress');
|
||||
|
||||
// Photo gallery
|
||||
// Photo gallery (metadata only, no image data)
|
||||
data.photoGallery = localStorage.getItem('photoGallery');
|
||||
data.capturedPhotos = localStorage.getItem('capturedPhotos');
|
||||
data.verificationPhotos = localStorage.getItem('verificationPhotos');
|
||||
|
||||
// Custom content
|
||||
data.customMainTasks = localStorage.getItem('customMainTasks');
|
||||
|
|
@ -335,16 +512,17 @@ class BackupManager {
|
|||
/**
|
||||
* Clean up old automatic backups
|
||||
*/
|
||||
cleanupOldBackups() {
|
||||
cleanupOldBackups(maxToKeep = null) {
|
||||
const backups = this.listBackups();
|
||||
const autoBackups = backups.filter(b => b.isAutomatic);
|
||||
const maxBackups = maxToKeep || this.maxAutoBackups;
|
||||
|
||||
if (autoBackups.length > this.maxAutoBackups) {
|
||||
const toDelete = autoBackups.slice(this.maxAutoBackups);
|
||||
if (autoBackups.length > maxBackups) {
|
||||
const toDelete = autoBackups.slice(maxBackups);
|
||||
toDelete.forEach(backup => {
|
||||
this.deleteBackup(backup.key);
|
||||
});
|
||||
console.log(`🧹 Cleaned up ${toDelete.length} old auto-backups`);
|
||||
console.log(`🧹 Cleaned up ${toDelete.length} old auto-backups (keeping ${maxBackups})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3087,16 +3087,31 @@
|
|||
// Store next step for completion callback
|
||||
window.trainingAcademyNextStep = nextStep;
|
||||
|
||||
// Emergency timeout: force close if verification takes too long (5 minutes)
|
||||
const emergencyTimeout = setTimeout(() => {
|
||||
console.warn('🚨 Emergency timeout: Force closing stuck verification');
|
||||
const overlay = document.getElementById('verification-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
// Clear any verification timers
|
||||
if (window.game?.webcamManager) {
|
||||
window.game.webcamManager.closeVerificationMode();
|
||||
}
|
||||
alert('Verification timed out. Skipping to next step.');
|
||||
proceedToNextStep(nextStep);
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
|
||||
// Store timeout reference for cleanup
|
||||
window.verificationEmergencyTimeout = emergencyTimeout;
|
||||
|
||||
// Create verification data for the webcam system
|
||||
const verificationData = {
|
||||
instructions: "Position verification required for training compliance",
|
||||
verificationInstructions: instructions,
|
||||
verificationText: verificationText,
|
||||
verificationDuration: duration,
|
||||
onComplete: function() {
|
||||
console.log('🔍 Position verification completed, proceeding to:', window.trainingAcademyNextStep);
|
||||
proceedToNextStep(window.trainingAcademyNextStep);
|
||||
}
|
||||
verificationDuration: duration
|
||||
// Note: Removed onComplete callback to avoid conflicts with event system
|
||||
};
|
||||
|
||||
// Use the webcam manager to start verification mode
|
||||
|
|
@ -3108,9 +3123,20 @@
|
|||
|
||||
// Listen for verification completion
|
||||
document.addEventListener('verificationComplete', function(event) {
|
||||
console.log('📨 Verification complete event received:', event.detail);
|
||||
|
||||
// Clear emergency timeout
|
||||
if (window.verificationEmergencyTimeout) {
|
||||
clearTimeout(window.verificationEmergencyTimeout);
|
||||
window.verificationEmergencyTimeout = null;
|
||||
console.log('⏰ Cleared emergency timeout');
|
||||
}
|
||||
|
||||
if (event.detail.success) {
|
||||
console.log('✅ Verification completed successfully');
|
||||
console.log('✅ Verification completed successfully, proceeding to:', nextStep);
|
||||
proceedToNextStep(nextStep);
|
||||
} else {
|
||||
console.warn('⚠️ Verification failed or was abandoned');
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
|
|
@ -3920,16 +3946,53 @@
|
|||
|
||||
// Create a simple test overlay
|
||||
const testOverlay = document.createElement('div');
|
||||
testOverlay.id = 'verification-test-overlay';
|
||||
testOverlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
`;
|
||||
testOverlay.innerHTML = `
|
||||
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.9); padding: 20px; border-radius: 10px; z-index: 99999;">
|
||||
<h3 style="color: white; margin-bottom: 15px;">🧪 Testing Photo Capture</h3>
|
||||
<div style="background: rgba(0,0,0,0.9); padding: 20px; border-radius: 10px; border: 2px solid #e74c3c;">
|
||||
<h3 style="color: white; margin-bottom: 15px; text-align: center;">🧪 Testing Photo Capture</h3>
|
||||
<video id="verification-video" autoplay muted playsinline style="width: 300px; height: 200px; border: 2px solid #e74c3c; border-radius: 8px; transform: scaleX(-1);"></video>
|
||||
<div id="verification-status" style="color: white; margin-top: 10px; text-align: center;">Preparing test...</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()" style="margin-top: 15px; padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer;">Close Test</button>
|
||||
<div style="text-align: center; margin-top: 15px;">
|
||||
<button id="close-verification-test" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer;">Close Test</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(testOverlay);
|
||||
|
||||
// Add proper close button handler
|
||||
const closeButton = testOverlay.querySelector('#close-verification-test');
|
||||
closeButton.addEventListener('click', () => {
|
||||
console.log('🧪 Closing verification test overlay...');
|
||||
if (testOverlay.parentNode) {
|
||||
testOverlay.parentNode.removeChild(testOverlay);
|
||||
}
|
||||
document.removeEventListener('keydown', escapeHandler);
|
||||
});
|
||||
|
||||
// Add escape key handler for emergency close
|
||||
const escapeHandler = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('🧪 Emergency closing verification test with Escape key...');
|
||||
if (testOverlay && testOverlay.parentNode) {
|
||||
testOverlay.parentNode.removeChild(testOverlay);
|
||||
}
|
||||
document.removeEventListener('keydown', escapeHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escapeHandler);
|
||||
|
||||
const testVideo = testOverlay.querySelector('#verification-video');
|
||||
if (window.game.webcamManager.stream) {
|
||||
testVideo.srcObject = window.game.webcamManager.stream;
|
||||
|
|
@ -3942,15 +4005,20 @@
|
|||
console.log('🧪 Test photo captured successfully!');
|
||||
const statusElement = testOverlay.querySelector('#verification-status');
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = '✅ Test photo captured! Check the gallery.';
|
||||
statusElement.innerHTML = '✅ Test photo captured! Check the gallery.<br><small>This overlay will auto-close in 5 seconds...</small>';
|
||||
statusElement.style.color = '#27ae60';
|
||||
}
|
||||
|
||||
// Refresh gallery button after a delay
|
||||
// Auto-close after showing success message
|
||||
setTimeout(() => {
|
||||
// Refresh the photo library to show new photos
|
||||
console.log('🧪 Auto-closing test overlay...');
|
||||
if (testOverlay && testOverlay.parentNode) {
|
||||
testOverlay.parentNode.removeChild(testOverlay);
|
||||
}
|
||||
|
||||
// Refresh gallery button after overlay is closed
|
||||
initializePhotoLibrary();
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue