feat: Add comprehensive photo gallery and management system
NEW FEATURES: Photo Gallery: - Accessible via new 'Photo Gallery' button on main menu - Grid view of all captured photos with thumbnails - Filter by session type (dress-up, studio, custom) - Sort by date (newest/oldest) or session type - Click photos to view full-size detail modal Persistent Photo Storage: - User consent dialog on first photo capture - Photos saved to localStorage with metadata - Privacy-first design (local storage only) - Automatic photo metadata tracking (date, session, task, size) Download Functionality: - Download individual photos as JPEG files - Batch download all photos (staggered to prevent browser blocking) - Proper filename generation with dates - Progress notifications during downloads Photo Management: - Delete individual photos with confirmation - Clear all photos option with confirmation - Photo storage settings modal - Storage statistics (count, size, date range) - Toggle photo storage consent on/off Professional UI: - Responsive photo grid layout - Hover effects and smooth transitions - Modal dialogs for photo details and settings - Filter and sort controls - Storage usage indicators - Empty state messaging TECHNICAL IMPROVEMENTS: - Enhanced WebcamManager with persistent storage methods - Comprehensive error handling and user feedback - Storage quota monitoring with warnings - Proper photo metadata structure - Browser-compatible download implementation Users can now capture photos during photography sessions and: View them later in a beautiful gallery Download their favorites Manage storage and privacy settings Organize by session type and date
This commit is contained in:
parent
13a76981be
commit
96c846cf8b
144
index.html
144
index.html
|
|
@ -62,6 +62,7 @@
|
|||
<button id="manage-tasks-btn" class="btn btn-secondary">Manage Tasks</button>
|
||||
<button id="manage-images-btn" class="btn btn-secondary">Manage Images</button>
|
||||
<button id="manage-audio-btn" class="btn btn-secondary">🎵 Manage Audio</button>
|
||||
<button id="photo-gallery-btn" class="btn btn-secondary">📸 Photo Gallery</button>
|
||||
<button id="manage-annoyance-btn" class="btn btn-secondary">😈 Annoyance</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -406,6 +407,149 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Gallery Screen -->
|
||||
<div id="photo-gallery-screen" class="screen">
|
||||
<h2>📸 Photo Gallery</h2>
|
||||
<p>View and manage your captured photos from photography sessions</p>
|
||||
|
||||
<!-- Gallery Controls -->
|
||||
<div class="gallery-controls">
|
||||
<div class="control-row">
|
||||
<div class="photo-stats">
|
||||
<span id="photo-count-display">Loading photos...</span>
|
||||
<span id="storage-size-display"></span>
|
||||
</div>
|
||||
<div class="photo-actions">
|
||||
<button id="download-all-photos-btn" class="btn btn-primary">📥 Download All</button>
|
||||
<button id="clear-all-photos-btn" class="btn btn-danger">🗑️ Clear All</button>
|
||||
<button id="photo-storage-settings-btn" class="btn btn-secondary">⚙️ Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Controls -->
|
||||
<div class="filter-controls">
|
||||
<label>Filter by Session Type:
|
||||
<select id="photo-session-filter">
|
||||
<option value="all">All Photos</option>
|
||||
<option value="dress-up-photo">Dress-up Photos</option>
|
||||
<option value="photography-studio">Photography Studio</option>
|
||||
<option value="custom">Custom Sessions</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Sort by:
|
||||
<select id="photo-sort-order">
|
||||
<option value="newest">Newest First</option>
|
||||
<option value="oldest">Oldest First</option>
|
||||
<option value="session">By Session Type</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Grid -->
|
||||
<div class="photo-grid-container">
|
||||
<div id="photo-grid" class="photo-grid">
|
||||
<!-- Photos will be populated here -->
|
||||
</div>
|
||||
<div id="no-photos-message" class="no-photos-message" style="display: none;">
|
||||
<h3>📷 No Photos Yet</h3>
|
||||
<p>Take some photos during photography sessions and they'll appear here!</p>
|
||||
<p>You can enable photo saving in the settings when you take your first photo.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Detail Modal -->
|
||||
<div id="photo-detail-modal" class="modal photo-modal" style="display: none;">
|
||||
<div class="modal-content photo-detail-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="photo-detail-title">Photo Details</h3>
|
||||
<span class="close" id="close-photo-detail">×</span>
|
||||
</div>
|
||||
<div class="modal-body photo-detail-body">
|
||||
<div class="photo-display">
|
||||
<img id="photo-detail-image" src="" alt="Photo" />
|
||||
</div>
|
||||
<div class="photo-info">
|
||||
<div class="info-row">
|
||||
<label>Captured:</label>
|
||||
<span id="photo-detail-date"></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<label>Session:</label>
|
||||
<span id="photo-detail-session"></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<label>Task:</label>
|
||||
<span id="photo-detail-task"></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<label>Size:</label>
|
||||
<span id="photo-detail-size"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="photo-actions">
|
||||
<button id="download-photo-btn" class="btn btn-primary">📥 Download</button>
|
||||
<button id="delete-photo-btn" class="btn btn-danger">🗑️ Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Storage Settings Modal -->
|
||||
<div id="photo-settings-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>📸 Photo Storage Settings</h3>
|
||||
<span class="close" id="close-photo-settings">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="setting-group">
|
||||
<h4>Storage Consent</h4>
|
||||
<div class="consent-controls">
|
||||
<label>
|
||||
<input type="radio" name="photo-consent" value="true" id="consent-enable">
|
||||
Enable photo storage (save photos for later viewing)
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="photo-consent" value="false" id="consent-disable">
|
||||
Disable photo storage (session only)
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-text">
|
||||
Photos are stored locally on your device only. No photos are sent to any servers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h4>Storage Information</h4>
|
||||
<div class="storage-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Total Photos:</span>
|
||||
<span id="settings-photo-count">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Storage Used:</span>
|
||||
<span id="settings-storage-size">0 KB</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Oldest Photo:</span>
|
||||
<span id="settings-oldest-photo">None</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button id="save-photo-settings-btn" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="management-buttons">
|
||||
<button id="back-to-start-from-photos-btn" class="btn btn-secondary">Back to Start</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Annoyance Management Screen -->
|
||||
<div id="annoyance-management-screen" class="screen">
|
||||
<h2>😈 Annoyance Management</h2>
|
||||
|
|
|
|||
260
src/core/game.js
260
src/core/game.js
|
|
@ -1658,6 +1658,9 @@ class TaskChallengeGame {
|
|||
// Audio management - only the main button, others will be attached when screen is shown
|
||||
document.getElementById('manage-audio-btn').addEventListener('click', () => this.showAudioManagement());
|
||||
|
||||
// Photo gallery management
|
||||
document.getElementById('photo-gallery-btn').addEventListener('click', () => this.showPhotoGallery());
|
||||
|
||||
// Annoyance management - main button and basic controls
|
||||
document.getElementById('manage-annoyance-btn').addEventListener('click', () => this.showAnnoyanceManagement());
|
||||
|
||||
|
|
@ -6128,6 +6131,263 @@ class MusicManager {
|
|||
// Set current selection
|
||||
trackSelector.value = this.currentTrackIndex;
|
||||
}
|
||||
|
||||
// Photo Gallery Management Methods
|
||||
showPhotoGallery() {
|
||||
this.showScreen('photo-gallery-screen');
|
||||
this.setupPhotoGalleryEventListeners();
|
||||
this.loadPhotoGallery();
|
||||
}
|
||||
|
||||
setupPhotoGalleryEventListeners() {
|
||||
// Back button
|
||||
const backBtn = document.getElementById('back-to-start-from-photos-btn');
|
||||
if (backBtn) {
|
||||
backBtn.onclick = () => this.showScreen('start-screen');
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
const downloadAllBtn = document.getElementById('download-all-photos-btn');
|
||||
if (downloadAllBtn) {
|
||||
downloadAllBtn.onclick = () => this.downloadAllPhotos();
|
||||
}
|
||||
|
||||
const clearAllBtn = document.getElementById('clear-all-photos-btn');
|
||||
if (clearAllBtn) {
|
||||
clearAllBtn.onclick = () => this.clearAllPhotos();
|
||||
}
|
||||
|
||||
const settingsBtn = document.getElementById('photo-storage-settings-btn');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.onclick = () => this.showPhotoSettings();
|
||||
}
|
||||
|
||||
// Filter controls
|
||||
const sessionFilter = document.getElementById('photo-session-filter');
|
||||
if (sessionFilter) {
|
||||
sessionFilter.onchange = () => this.filterPhotos();
|
||||
}
|
||||
|
||||
const sortOrder = document.getElementById('photo-sort-order');
|
||||
if (sortOrder) {
|
||||
sortOrder.onchange = () => this.filterPhotos();
|
||||
}
|
||||
|
||||
// Modal close buttons
|
||||
const closeDetailBtn = document.getElementById('close-photo-detail');
|
||||
if (closeDetailBtn) {
|
||||
closeDetailBtn.onclick = () => this.closePhotoDetail();
|
||||
}
|
||||
|
||||
const closeSettingsBtn = document.getElementById('close-photo-settings');
|
||||
if (closeSettingsBtn) {
|
||||
closeSettingsBtn.onclick = () => this.closePhotoSettings();
|
||||
}
|
||||
|
||||
// Photo detail modal actions
|
||||
const downloadBtn = document.getElementById('download-photo-btn');
|
||||
if (downloadBtn) {
|
||||
downloadBtn.onclick = () => this.downloadCurrentPhoto();
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('delete-photo-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.onclick = () => this.deleteCurrentPhoto();
|
||||
}
|
||||
|
||||
// Settings modal actions
|
||||
const saveSettingsBtn = document.getElementById('save-photo-settings-btn');
|
||||
if (saveSettingsBtn) {
|
||||
saveSettingsBtn.onclick = () => this.savePhotoSettings();
|
||||
}
|
||||
}
|
||||
|
||||
loadPhotoGallery() {
|
||||
const stats = this.webcamManager.getPhotoStats();
|
||||
this.updatePhotoStats(stats);
|
||||
|
||||
const photos = this.webcamManager.getSavedPhotos();
|
||||
const grid = document.getElementById('photo-grid');
|
||||
const noPhotosMsg = document.getElementById('no-photos-message');
|
||||
|
||||
if (photos.length === 0) {
|
||||
grid.style.display = 'none';
|
||||
noPhotosMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.style.display = 'grid';
|
||||
noPhotosMsg.style.display = 'none';
|
||||
|
||||
this.renderPhotoGrid(photos);
|
||||
}
|
||||
|
||||
updatePhotoStats(stats) {
|
||||
const countDisplay = document.getElementById('photo-count-display');
|
||||
const sizeDisplay = document.getElementById('storage-size-display');
|
||||
|
||||
if (countDisplay) {
|
||||
countDisplay.textContent = `${stats.count} photos`;
|
||||
}
|
||||
|
||||
if (sizeDisplay) {
|
||||
const sizeInKB = (stats.totalSize / 1024).toFixed(1);
|
||||
sizeDisplay.textContent = `${sizeInKB} KB used`;
|
||||
}
|
||||
}
|
||||
|
||||
renderPhotoGrid(photos) {
|
||||
const grid = document.getElementById('photo-grid');
|
||||
const sessionFilter = document.getElementById('photo-session-filter').value;
|
||||
const sortOrder = document.getElementById('photo-sort-order').value;
|
||||
|
||||
// Filter photos
|
||||
let filteredPhotos = photos;
|
||||
if (sessionFilter !== 'all') {
|
||||
filteredPhotos = photos.filter(photo => photo.sessionType === sessionFilter);
|
||||
}
|
||||
|
||||
// Sort photos
|
||||
switch (sortOrder) {
|
||||
case 'newest':
|
||||
filteredPhotos.sort((a, b) => b.timestamp - a.timestamp);
|
||||
break;
|
||||
case 'oldest':
|
||||
filteredPhotos.sort((a, b) => a.timestamp - b.timestamp);
|
||||
break;
|
||||
case 'session':
|
||||
filteredPhotos.sort((a, b) => a.sessionType.localeCompare(b.sessionType));
|
||||
break;
|
||||
}
|
||||
|
||||
// Render photo items
|
||||
grid.innerHTML = filteredPhotos.map(photo => this.createPhotoItem(photo)).join('');
|
||||
|
||||
// Add click listeners
|
||||
grid.querySelectorAll('.photo-item').forEach(item => {
|
||||
const photoId = item.dataset.photoId;
|
||||
item.onclick = () => this.showPhotoDetail(photoId);
|
||||
});
|
||||
}
|
||||
|
||||
createPhotoItem(photo) {
|
||||
const date = new Date(photo.timestamp).toLocaleDateString();
|
||||
const sessionName = this.formatSessionName(photo.sessionType);
|
||||
|
||||
return `
|
||||
<div class="photo-item" data-photo-id="${photo.id}">
|
||||
<img src="${photo.dataURL}" alt="Captured photo" loading="lazy">
|
||||
<div class="photo-item-info">
|
||||
<div class="photo-item-date">${date}</div>
|
||||
<div class="photo-item-session">${sessionName}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
formatSessionName(sessionType) {
|
||||
const sessionNames = {
|
||||
'dress-up-photo': 'Dress-up',
|
||||
'photography-studio': 'Studio',
|
||||
'custom': 'Custom'
|
||||
};
|
||||
return sessionNames[sessionType] || sessionType;
|
||||
}
|
||||
|
||||
filterPhotos() {
|
||||
const photos = this.webcamManager.getSavedPhotos();
|
||||
this.renderPhotoGrid(photos);
|
||||
}
|
||||
|
||||
showPhotoDetail(photoId) {
|
||||
const photos = this.webcamManager.getSavedPhotos();
|
||||
const photo = photos.find(p => p.id === photoId);
|
||||
if (!photo) return;
|
||||
|
||||
this.currentDetailPhoto = photo;
|
||||
|
||||
// Populate modal
|
||||
document.getElementById('photo-detail-image').src = photo.dataURL;
|
||||
document.getElementById('photo-detail-date').textContent = new Date(photo.timestamp).toLocaleString();
|
||||
document.getElementById('photo-detail-session').textContent = this.formatSessionName(photo.sessionType);
|
||||
document.getElementById('photo-detail-task').textContent = photo.taskId || 'Unknown';
|
||||
document.getElementById('photo-detail-size').textContent = `${Math.round(photo.size / 1024)} KB`;
|
||||
|
||||
// Show modal
|
||||
document.getElementById('photo-detail-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
closePhotoDetail() {
|
||||
document.getElementById('photo-detail-modal').style.display = 'none';
|
||||
this.currentDetailPhoto = null;
|
||||
}
|
||||
|
||||
downloadCurrentPhoto() {
|
||||
if (this.currentDetailPhoto) {
|
||||
this.webcamManager.downloadPhoto(this.currentDetailPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
deleteCurrentPhoto() {
|
||||
if (!this.currentDetailPhoto) return;
|
||||
|
||||
if (confirm('Are you sure you want to delete this photo? This cannot be undone.')) {
|
||||
const success = this.webcamManager.deletePhoto(this.currentDetailPhoto.id);
|
||||
if (success) {
|
||||
this.closePhotoDetail();
|
||||
this.loadPhotoGallery(); // Refresh the gallery
|
||||
this.webcamManager.showNotification('Photo deleted', 'success');
|
||||
} else {
|
||||
this.webcamManager.showNotification('Failed to delete photo', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadAllPhotos() {
|
||||
this.webcamManager.downloadAllPhotos();
|
||||
}
|
||||
|
||||
clearAllPhotos() {
|
||||
if (confirm('Are you sure you want to delete ALL photos? This cannot be undone.')) {
|
||||
const success = this.webcamManager.clearAllPhotos();
|
||||
if (success) {
|
||||
this.loadPhotoGallery(); // Refresh the gallery
|
||||
this.webcamManager.showNotification('All photos cleared', 'success');
|
||||
} else {
|
||||
this.webcamManager.showNotification('Failed to clear photos', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showPhotoSettings() {
|
||||
const stats = this.webcamManager.getPhotoStats();
|
||||
|
||||
// Update consent radio buttons
|
||||
const consentValue = stats.storageConsent;
|
||||
document.getElementById('consent-enable').checked = consentValue === 'true';
|
||||
document.getElementById('consent-disable').checked = consentValue === 'false';
|
||||
|
||||
// Update stats
|
||||
document.getElementById('settings-photo-count').textContent = stats.count;
|
||||
document.getElementById('settings-storage-size').textContent = `${(stats.totalSize / 1024).toFixed(1)} KB`;
|
||||
document.getElementById('settings-oldest-photo').textContent =
|
||||
stats.oldestPhoto ? new Date(stats.oldestPhoto).toLocaleDateString() : 'None';
|
||||
|
||||
// Show modal
|
||||
document.getElementById('photo-settings-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
closePhotoSettings() {
|
||||
document.getElementById('photo-settings-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
savePhotoSettings() {
|
||||
const consentValue = document.querySelector('input[name="photo-consent"]:checked').value;
|
||||
localStorage.setItem('photoStorageConsent', consentValue);
|
||||
|
||||
this.closePhotoSettings();
|
||||
this.webcamManager.showNotification('Photo settings saved', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Annoyance Management Methods - Phase 2: Advanced Message Management
|
||||
|
|
|
|||
|
|
@ -536,20 +536,254 @@ class WebcamManager {
|
|||
* Save photo data (respecting privacy)
|
||||
*/
|
||||
savePhotoData(photoData) {
|
||||
// Only save metadata by default for privacy
|
||||
// Save metadata to session storage (temporary)
|
||||
const metadata = {
|
||||
timestamp: photoData.timestamp,
|
||||
sessionType: photoData.sessionType,
|
||||
taskId: photoData.taskData?.id
|
||||
};
|
||||
|
||||
// Save to session storage (temporary)
|
||||
const sessionPhotos = JSON.parse(sessionStorage.getItem('photoSession') || '[]');
|
||||
sessionPhotos.push(metadata);
|
||||
sessionStorage.setItem('photoSession', JSON.stringify(sessionPhotos));
|
||||
|
||||
// Option for user to save actual photos locally
|
||||
// (This would need explicit user consent)
|
||||
// Save actual photo data to localStorage with user consent
|
||||
this.savePersistentPhoto(photoData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save photo data persistently with user consent
|
||||
*/
|
||||
savePersistentPhoto(photoData) {
|
||||
try {
|
||||
// Check if user has given consent for photo storage
|
||||
const photoStorageConsent = localStorage.getItem('photoStorageConsent');
|
||||
|
||||
if (photoStorageConsent === null) {
|
||||
// First time - ask for consent
|
||||
this.requestPhotoStorageConsent(photoData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (photoStorageConsent === 'true') {
|
||||
// User has consented - save the photo
|
||||
this.storePhotoInLocalStorage(photoData);
|
||||
}
|
||||
// If consent is 'false', don't save (but still allow session use)
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to save photo persistently:', error);
|
||||
// Continue without persistent storage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user consent for photo storage
|
||||
*/
|
||||
requestPhotoStorageConsent(photoData) {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7); display: flex; align-items: center;
|
||||
justify-content: center; z-index: 10000;
|
||||
`;
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="background: white; padding: 30px; border-radius: 15px; max-width: 500px; text-align: center;">
|
||||
<h3>📸 Save Photos for Later?</h3>
|
||||
<p>Would you like to save your captured photos so you can view and download them later?</p>
|
||||
<p style="font-size: 0.9em; color: #666; margin: 15px 0;">
|
||||
Photos are stored locally on your device only. You can change this setting anytime in options.
|
||||
</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<button id="consent-yes" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin: 0 10px; cursor: pointer;">
|
||||
Yes, Save Photos
|
||||
</button>
|
||||
<button id="consent-no" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin: 0 10px; cursor: pointer;">
|
||||
No, Session Only
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById('consent-yes').onclick = () => {
|
||||
localStorage.setItem('photoStorageConsent', 'true');
|
||||
this.storePhotoInLocalStorage(photoData);
|
||||
document.body.removeChild(modal);
|
||||
this.showNotification('📸 Photos will be saved for later viewing!', 'success');
|
||||
};
|
||||
|
||||
document.getElementById('consent-no').onclick = () => {
|
||||
localStorage.setItem('photoStorageConsent', 'false');
|
||||
document.body.removeChild(modal);
|
||||
this.showNotification('Photos will only be available during this session', 'info');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store photo in localStorage
|
||||
*/
|
||||
storePhotoInLocalStorage(photoData) {
|
||||
try {
|
||||
const savedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
|
||||
const photoToSave = {
|
||||
id: `photo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
dataURL: photoData.dataURL,
|
||||
timestamp: photoData.timestamp,
|
||||
sessionType: photoData.sessionType,
|
||||
taskId: photoData.taskData?.id || 'unknown',
|
||||
dateCreated: new Date(photoData.timestamp).toISOString(),
|
||||
size: Math.round(photoData.dataURL.length * 0.75) // Approximate size in bytes
|
||||
};
|
||||
|
||||
savedPhotos.push(photoToSave);
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(savedPhotos));
|
||||
|
||||
console.log(`📸 Photo saved persistently (ID: ${photoToSave.id})`);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to save photo to localStorage:', error);
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
this.showNotification('⚠️ Storage full - consider downloading and clearing old photos', 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification helper
|
||||
*/
|
||||
showNotification(message, type = 'info', duration = 3000) {
|
||||
// Create notification if it doesn't exist
|
||||
let notification = document.getElementById('photo-notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.id = 'photo-notification';
|
||||
notification.style.cssText = `
|
||||
position: fixed; top: 20px; right: 20px; padding: 15px 20px;
|
||||
border-radius: 8px; color: white; z-index: 5000; font-weight: bold;
|
||||
transition: all 0.3s ease; opacity: 0; transform: translateX(100%);
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
|
||||
// Set message and style based on type
|
||||
notification.textContent = message;
|
||||
const colors = {
|
||||
success: '#28a745',
|
||||
warning: '#ffc107',
|
||||
error: '#dc3545',
|
||||
info: '#17a2b8'
|
||||
};
|
||||
notification.style.background = colors[type] || colors.info;
|
||||
|
||||
// Show notification
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateX(0)';
|
||||
|
||||
// Hide after duration
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all saved photos from localStorage
|
||||
*/
|
||||
getSavedPhotos() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to retrieve saved photos:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific photo by ID
|
||||
*/
|
||||
deletePhoto(photoId) {
|
||||
try {
|
||||
const savedPhotos = this.getSavedPhotos();
|
||||
const filteredPhotos = savedPhotos.filter(photo => photo.id !== photoId);
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(filteredPhotos));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to delete photo:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all saved photos
|
||||
*/
|
||||
clearAllPhotos() {
|
||||
try {
|
||||
localStorage.removeItem('capturedPhotos');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to clear photos:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a photo as a file
|
||||
*/
|
||||
downloadPhoto(photo, filename = null) {
|
||||
try {
|
||||
const link = document.createElement('a');
|
||||
link.download = filename || `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
|
||||
link.href = photo.dataURL;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to download photo:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all photos as a zip (simplified version - individual downloads)
|
||||
*/
|
||||
downloadAllPhotos() {
|
||||
const photos = this.getSavedPhotos();
|
||||
if (photos.length === 0) {
|
||||
this.showNotification('No photos to download', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
photos.forEach((photo, index) => {
|
||||
setTimeout(() => {
|
||||
this.downloadPhoto(photo, `photo_${index + 1}_${new Date(photo.timestamp).toISOString().slice(0, 10)}.jpg`);
|
||||
}, index * 500); // Stagger downloads
|
||||
});
|
||||
|
||||
this.showNotification(`📥 Downloading ${photos.length} photos...`, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get photo storage statistics
|
||||
*/
|
||||
getPhotoStats() {
|
||||
const photos = this.getSavedPhotos();
|
||||
const totalSize = photos.reduce((sum, photo) => sum + (photo.size || 0), 0);
|
||||
|
||||
return {
|
||||
count: photos.length,
|
||||
totalSize: totalSize,
|
||||
oldestPhoto: photos.length > 0 ? Math.min(...photos.map(p => p.timestamp)) : null,
|
||||
newestPhoto: photos.length > 0 ? Math.max(...photos.map(p => p.timestamp)) : null,
|
||||
sessionTypes: [...new Set(photos.map(p => p.sessionType))],
|
||||
storageConsent: localStorage.getItem('photoStorageConsent')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3758,4 +3758,225 @@ body.theme-monochrome {
|
|||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Photo Gallery Styles */
|
||||
.photo-grid-container {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.photo-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.photo-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-item-info {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.photo-item-date {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.photo-item-session {
|
||||
font-size: 0.8em;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.no-photos-message {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-photos-message h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Photo Gallery Controls */
|
||||
.gallery-controls {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.photo-stats {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.photo-stats span {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.photo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-controls label {
|
||||
font-size: 0.9em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-controls select {
|
||||
margin-left: 8px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Photo Detail Modal */
|
||||
.photo-modal .modal-content {
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.photo-detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.photo-detail-body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.photo-display {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.photo-display img {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.photo-info {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-row label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.photo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Photo Settings Modal */
|
||||
.setting-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.setting-group h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.consent-controls {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.consent-controls label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.consent-controls input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.storage-info {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.85em;
|
||||
color: #6c757d;
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
Loading…
Reference in New Issue