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:
dilgenfritz 2025-10-28 07:49:55 -05:00
parent 13a76981be
commit 96c846cf8b
4 changed files with 863 additions and 4 deletions

View File

@ -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">&times;</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">&times;</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>

View File

@ -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

View File

@ -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')
};
}
/**

View File

@ -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;
}