added video functionality and minor bug fixes.

This commit is contained in:
dilgenfritz 2025-10-29 14:30:42 -05:00
parent 559c9e69f1
commit f08bfbb5cc
12 changed files with 1698 additions and 201 deletions

10
.gitignore vendored
View File

@ -45,7 +45,15 @@ images/consequences/*
*.aac
*.flac
*.wma
videos/tasks/*
videos/punishments/*
videos/rewards/*
videos/background/*
*.mp4
*.avi
*.mov
*.wmv
*.flv
# Backup files
*.bak
*~

245
README.md
View File

@ -2,198 +2,109 @@
*How long can you last?*
An edging challenge game where skipping tasks floods your screen with consequences. Test your willpower against an unforgiving system that punishes procrastination with visual torment.
An adult edging challenge game with multiple specialized modes, progressive training systems, and customizable punishment mechanics.
## Features
## 🎮 Game Modes
### 🔥 Core Gameplay
- **Edging Challenge System**: Push your limits with timed endurance tasks
- **Punishment Protocol**: Skip tasks and face flooding consequence popups
- **Dynamic Consequences**: 1-40 popup images that can't be closed, sized to image ratios
- **Smart Timing**: Adaptive delays and duration controls for maximum impact
- **Willpower Training**: Progressive difficulty that builds self-control
- **Standard Game** - Classic endless task mode
- **Timed Challenge** - Race against the clock
- **Score Target** - Reach target points to win
- **Training Academy** - Gooning-focused training with progressive scenarios
- **Punishment Gauntlet** - Intense humiliation challenges
- **Endurance Trials** - Progressive endurance training (30s to 10min sessions)
- **Photography Studio** - Webcam photography and posing sessions
### 💥 Punishment Features
- **Popup Image Flood**: Overwhelm your screen with consequence images
- **Aspect Ratio Sizing**: Each popup perfectly sized to its image proportions
- **Configurable Torment**: Control count (1-40), duration, positioning, and effects
- **Unclosable Design**: Popups can't be dismissed - you must endure the timer
- **Visual Annoyance System**: Blur backgrounds, fade animations, countdown timers
## <20> Core Features
### 🎯 Annoyance Management
- **Flash Message System**: Encouraging/taunting messages during gameplay
- **Message Customization**: 20+ default messages across 4 categories
- **Advanced Scheduling**: Frequency controls, event triggers, interval timing
- **Import/Export**: Share message collections and settings
### Progressive Endurance Training
- **Experience Levels**: Beginner → Intermediate → Advanced → Expert
- **Adaptive Timers**: 30 seconds to 10 minutes based on skill level
- **Certification System**: Graduate through progressive difficulty
- **Timer-Based Focus**: Performance tasks without minigames
### 🎨 Customization
- **Theme System**: Multiple visual themes (midnight, ocean, forest, etc.)
- **Background Music**: 4 music tracks with loop/shuffle controls
- **Volume Control**: Adjustable audio settings
- **NSFW Image Management**: Upload and manage custom task/consequence images
- **Size Controls**: Configure popup dimensions and viewport usage
- **Positioning Options**: Random, cascade, grid, or centered popup layouts
### Interactive Task System
- **Mirror Tasks**: Webcam-based self-humiliation challenges
- **Focus Holds**: Timed endurance and concentration exercises
- **Scenario Adventures**: Choose-your-own-adventure style content
- **Photography Sessions**: Webcam capture with posing instructions
### 🖼️ Advanced Image System
- **Categorized Images**: Separate adult task and consequence image pools
- **High-Quality Processing**: 1600x1200 resolution with 95% JPEG quality
- **Smart Compression**: Automatic resizing for optimal storage
- **Batch Operations**: Upload multiple images, select/delete in bulk
- **Storage Management**: 50 image limit with quota monitoring
- **Format Support**: JPG, PNG, WebP image formats up to 20MB
### Punishment System
- **Popup Floods**: 1-40 consequence images that can't be closed
- **Smart Sizing**: Auto-sized to image aspect ratios
- **Configurable Timing**: 3-30 second durations with random variations
- **Visual Effects**: Background blur, fade animations, countdown timers
### 🛠️ Technical Features
- **Local Storage**: All data stored locally in browser
- **Responsive Design**: Works on desktop and mobile
- **Cross-Browser Compatible**: Modern browser support
- **No Backend Required**: Pure client-side application
### Content Management
- **Custom Images**: Upload and categorize adult content (tasks vs consequences)
- **High-Quality Processing**: 1600x1200 resolution with smart compression
- **Storage Management**: 50 image limit with usage monitoring
- **Bulk Operations**: Select, enable/disable, and delete multiple images
## Getting Started
## <EFBFBD> Quick Start
### Quick Start
1. Open `index.html` in a modern web browser
2. Choose your theme and begin the challenge
3. Upload custom adult images through the Image Management screen
4. Configure punishment settings in Annoyance Management
### For Electron App
### Desktop App (Recommended)
```bash
# Install dependencies
npm install
# Run the application
npm start
```
### Local Development
```bash
# Clone the repository
git clone [repository-url]
cd webGame
### Web Browser
1. Open `index.html` in a modern browser
2. Choose your game mode and begin
3. Upload custom content via Image Management
4. Configure settings in Options menu
# Serve locally (recommended)
python -m http.server 8000
# OR
npx serve .
## 🎯 Key Controls
# Open browser to http://localhost:8000
```
- **Enter** - Complete task
- **Ctrl** - Skip task (triggers punishment)
- **Space/P** - Pause/resume
- **M** - Toggle music
- **H** - Help menu
- **Escape** - Close dialogs
## File Structure
## 🛠️ Technical
### Requirements
- Modern web browser with webcam support
- Local storage for saves and images
- 50MB+ available storage for custom content
### File Structure
```
webGame/
├── index.html # Main game interface
├── game.js # Core game logic and punishment system
├── gameData.js # Game data and configuration
├── flashMessageManager.js # Flash message system
├── popupImageManager.js # Punishment popup system
├── styles.css # All styling and themes
├── main.js # Electron main process
├── preload.js # Electron preload script
├── package.json # App configuration and dependencies
├── images/
│ ├── tasks/ # Default adult task images
│ ├── consequences/ # Default consequence images
│ └── cached/ # User-uploaded processed images
├── audio/ # Background music files
└── README.md # This file
├── index.html # Main interface
├── src/
│ ├── core/ # Game engine and mode management
│ ├── data/modes/ # Game mode data files
│ ├── features/ # Task system, webcam, audio
│ └── styles/ # CSS and themes
├── images/ # Image storage
├── audio/ # Background music
└── package.json # Electron app config
```
## Image Management
### Features
- **Cross-Platform**: Windows, Mac, Linux via Electron
- **Responsive Design**: Works on desktop and mobile
- **Local Storage**: All data stays on your device
- **Privacy-First**: No data sent to servers
### Upload Process
1. Navigate to Image Management screen
2. Switch between "Task Images" and "Consequence Images" tabs
3. Click "Upload Images" to select files
4. Images are automatically processed and optimized
## 📋 Recent Updates
### Storage System
- **Quality**: High-quality compression (1600x1200, 95% JPEG quality)
- **Limits**: 50 total images to prevent storage issues
- **Categories**: Separate pools for tasks vs consequences
- **Formats**: Supports JPG, PNG, WebP up to 20MB each
### v2.0 - Major Content Update
- ✅ Added 7 specialized game modes with rich content
- ✅ Implemented progressive endurance training system
- ✅ Enhanced interactive task management
- ✅ Webcam integration for mirror and photography tasks
- ✅ Modular data system for better organization
- ✅ Removed deprecated scenario mode
### Management Features
- **Enable/Disable**: Control which images appear in game
- **Bulk Operations**: Select all, delete multiple images
- **Storage Info**: Monitor usage and get cleanup suggestions
## Game Controls
### Mouse Controls
- Click buttons for all game actions
- Navigate punishment/annoyance management screens
- Configure popup settings and test functionality
### Keyboard Shortcuts
- **Enter**: Complete current task
- **Ctrl**: Skip current task (triggers punishment)
- **Shift+Ctrl**: Mercy skip (if available)
- **Space/P**: Pause/resume challenge
- **M**: Toggle background music
- **H**: Show help
- **Escape**: Close dialogs/go back
## Punishment System
### Popup Configuration
- **Count**: 1-40 images (fixed, random 1-10, or custom range)
- **Duration**: 3-30 seconds (fixed, random 5-15s, or custom range)
- **Positioning**: Random, cascade, grid, or centered layouts
- **Size**: Auto-sized to image aspect ratios with configurable limits
- **Effects**: Fade animations, background blur, countdown timers
### Visual Impact
- **Unclosable**: Popups cannot be dismissed manually
- **Screen Flooding**: Up to 40 consequence images simultaneously
- **Perfect Sizing**: Each popup sized to match its image proportions
- **Smart Spacing**: Collision detection prevents visual chaos
## Development History
### Recent Updates (v1.4)
- ✅ Implemented categorized image system (Task/Consequence separation)
- ✅ Enhanced image processing with higher quality compression
- ✅ Added storage quota management and limits
- ✅ Fixed tab switching and individual image selection
- ✅ Improved error handling and user feedback
- ✅ Added batch upload and management features
### Technical Architecture
- **DataManager Class**: Centralized localStorage management
- **TaskChallengeGame Class**: Main game logic and state management
- **Image Processing**: Canvas-based compression and optimization
- **Event System**: Comprehensive keyboard and mouse event handling
- **Theme System**: CSS-based visual customization
## Browser Compatibility
- ✅ Chrome/Chromium (recommended)
- ✅ Firefox
- ✅ Safari
- ✅ Edge
- ⚠️ IE11+ (limited support)
## Contributing
### Development Setup
1. Clone repository
2. Use local server for testing (file:// protocol has limitations)
3. Test across different browsers
4. Follow existing code structure and commenting style
### Code Organization
- `game.js`: Main game logic, image management, data persistence
- `styles.css`: All styling including themes and responsive design
- `index.html`: Clean semantic HTML structure
## Version History
- **v1.0**: Initial implementation with basic game mechanics
- **v1.1**: Added theme system and music controls
- **v1.2**: Implemented image upload and management
- **v1.3**: Enhanced image processing and categorization
- **v1.4**: Added storage management and quota limits (current)
### v1.4 - Enhanced Image System
- ✅ Categorized image management (tasks vs consequences)
- ✅ High-quality compression and processing
- ✅ Storage quota management
- ✅ Bulk operations and selection
## License

49
debug-video.js Normal file
View File

@ -0,0 +1,49 @@
// Debug script to test video player integration
console.log('🔍 Testing video player integration...');
// Test if button exists
const videoBtn = document.getElementById('manage-video-btn');
console.log('Video button exists:', !!videoBtn);
// Test if video management screen exists
const videoScreen = document.getElementById('video-management-screen');
console.log('Video management screen exists:', !!videoScreen);
// Test if VideoPlayerManager class is available
console.log('VideoPlayerManager class available:', typeof window.VideoPlayerManager !== 'undefined');
// Test if showScreen function is available
console.log('showScreen function available:', typeof window.game?.showScreen === 'function');
// Manual button click test
if (videoBtn) {
console.log('✅ Button found, adding click test...');
videoBtn.addEventListener('click', () => {
console.log('🎬 Video button clicked!');
// Try to show the video management screen
if (window.game && typeof window.game.showScreen === 'function') {
console.log('Calling showScreen...');
window.game.showScreen('video-management-screen');
} else {
console.error('❌ showScreen function not available');
}
});
} else {
console.error('❌ Video button not found');
}
// Test function directly
function testVideoScreen() {
console.log('🧪 Testing direct screen switch...');
if (window.game && typeof window.game.showScreen === 'function') {
window.game.showScreen('video-management-screen');
} else {
console.error('❌ Game instance or showScreen not available');
}
}
// Make test function available globally
window.testVideoScreen = testVideoScreen;
console.log('✅ Debug script loaded. Try: window.testVideoScreen()');

View File

@ -105,6 +105,10 @@ class TaskChallengeGame {
window.gameModeManager = this.gameModeManager;
this.updateLoadingProgress(75, 'Game mode manager loaded...');
// Initialize Video Player Manager
this.initializeVideoManager();
this.updateLoadingProgress(78, 'Video manager initialized...');
// Initialize webcam for photography tasks
this.initializeWebcam();
this.updateLoadingProgress(80, 'Webcam initialized...');
@ -198,6 +202,24 @@ class TaskChallengeGame {
}
}
initializeVideoManager() {
// Initialize video player manager if available
if (typeof VideoPlayerManager !== 'undefined') {
if (!window.videoPlayerManager) {
window.videoPlayerManager = new VideoPlayerManager();
console.log('🎬 Video Player Manager created');
}
// Initialize the video system
if (window.videoPlayerManager && typeof window.videoPlayerManager.init === 'function') {
window.videoPlayerManager.init();
console.log('🎬 Video Player Manager initialized');
}
} else {
console.warn('⚠️ VideoPlayerManager not available, video features will be disabled');
}
}
async initDesktopFeatures() {
// Initialize desktop file manager
if (typeof DesktopFileManager !== 'undefined') {

View File

@ -356,7 +356,23 @@ class GameModeManager {
// Combine tasks and scenarios into the format expected by the game
const allTasks = [];
// Add regular tasks
// Add scenarios FIRST (so they get priority in selection)
scenarios.forEach(scenario => {
const gameTask = {
id: scenario.id,
text: scenario.text,
difficulty: scenario.difficulty,
type: 'main',
interactiveType: scenario.interactiveType,
interactiveData: scenario.interactiveData,
isScenario: true,
image: this.getRandomScenarioImage()
};
allTasks.push(gameTask);
console.log(`✅ Added scenario: ${scenario.id} (${scenario.interactiveType})`);
});
// Add regular tasks AFTER scenarios
tasks.forEach(task => {
const gameTask = {
id: task.id,
@ -374,22 +390,6 @@ class GameModeManager {
console.log(`✅ Added task: ${task.id} (${task.interactiveType})`);
});
// Add scenarios
scenarios.forEach(scenario => {
const gameTask = {
id: scenario.id,
text: scenario.text,
difficulty: scenario.difficulty,
type: 'main',
interactiveType: scenario.interactiveType,
interactiveData: scenario.interactiveData,
isScenario: true,
image: this.getRandomScenarioImage()
};
allTasks.push(gameTask);
console.log(`✅ Added scenario: ${scenario.id} (${scenario.interactiveType})`);
});
console.log(`🎯 Created ${allTasks.length} total tasks for ${this.currentMode}`);
return allTasks;
}

View File

@ -254,4 +254,81 @@ ipcMain.handle('copy-audio', async (event, sourcePath, destPath) => {
console.error('Error copying audio file:', error);
return false;
}
});
// Video file operations
ipcMain.handle('select-videos', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
filters: [
{ name: 'Video Files', extensions: ['mp4', 'webm', 'ogv', 'mov', 'avi', 'mkv'] }
]
});
if (!result.canceled) {
return result.filePaths;
}
return [];
});
ipcMain.handle('read-video-directory', async (event, dirPath) => {
try {
const files = await fs.readdir(dirPath);
const videoFiles = files.filter(file => {
const ext = path.extname(file).toLowerCase();
return ['.mp4', '.webm', '.ogv', '.mov', '.avi', '.mkv'].includes(ext);
});
const videoFilePromises = videoFiles.map(async file => {
const filePath = path.join(dirPath, file);
const stats = await fs.stat(filePath);
const ext = path.extname(file).toLowerCase();
// Map file extension to MIME type
const mimeTypeMap = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogv': 'video/ogg',
'.mov': 'video/quicktime',
'.avi': 'video/x-msvideo',
'.mkv': 'video/x-matroska'
};
return {
name: file,
path: filePath,
url: `file:///${filePath.replace(/\\/g, '/')}`,
title: file.replace(/\.[^/.]+$/, "").replace(/[-_]/g, ' ').replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()),
size: stats.size,
type: mimeTypeMap[ext] || 'video/mp4'
};
});
// Wait for all async operations to complete
return Promise.all(videoFilePromises);
} catch (error) {
console.error('Error reading video directory:', error);
return [];
}
});
ipcMain.handle('copy-video', async (event, sourcePath, destPath) => {
try {
await fs.mkdir(path.dirname(destPath), { recursive: true });
await fs.copyFile(sourcePath, destPath);
return true;
} catch (error) {
console.error('Error copying video file:', error);
return false;
}
});
ipcMain.handle('delete-video', async (event, filePath) => {
try {
await fs.unlink(filePath);
return true;
} catch (error) {
console.error('Error deleting video file:', error);
return false;
}
});

View File

@ -14,6 +14,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
readAudioDirectory: (dirPath) => ipcRenderer.invoke('read-audio-directory', dirPath),
copyAudio: (sourcePath, destPath) => ipcRenderer.invoke('copy-audio', sourcePath, destPath),
// Video file operations
selectVideos: () => ipcRenderer.invoke('select-videos'),
readVideoDirectory: (dirPath) => ipcRenderer.invoke('read-video-directory', dirPath),
copyVideo: (sourcePath, destPath) => ipcRenderer.invoke('copy-video', sourcePath, destPath),
deleteVideo: (filePath) => ipcRenderer.invoke('delete-video', filePath),
// File system utilities
getAppPath: () => ipcRenderer.invoke('get-app-path'),
pathJoin: (...paths) => ipcRenderer.invoke('path-join', ...paths),

View File

@ -114,6 +114,40 @@ const trainingGameData = {
effects: { arousal: 25, control: 15 },
nextStep: "edging_assessment"
},
pattern_edging: {
type: 'action',
mood: 'technical',
story: "Your instructor demonstrates complex stroking patterns. 'Advanced gooners master sophisticated rhythms and techniques. You'll learn variable speed patterns, pressure control, and rhythm changes that maximize arousal while maintaining perfect edge control.'",
actionText: "Advanced edging pattern training",
duration: 150,
effects: { arousal: 20, control: 15 },
nextStep: "edging_assessment"
},
edging_assessment: {
type: 'choice',
mood: 'evaluative',
story: "Your instructor evaluates your edging progress. 'Good foundation! Now let's see how ready you are for more advanced training. Your edging skills are developing nicely. What aspect would you like to focus on next?'",
choices: [
{
text: "Continue building endurance",
preview: "Work on longer edging sessions",
effects: { arousal: 15 },
nextStep: "endurance_mastery"
},
{
text: "Move to advanced training",
preview: "Progress to expert techniques",
effects: { arousal: 10, control: 5 },
nextStep: "advanced_training"
},
{
text: "Explore different training paths",
preview: "Try other aspects of gooner training",
effects: { arousal: 5 },
nextStep: "addiction_path"
}
]
},
marathon_edging: {
type: 'action',
mood: 'intense',
@ -157,6 +191,67 @@ const trainingGameData = {
effects: { arousal: 35, control: -15 },
nextStep: "addiction_deepening"
},
dependency_training: {
type: 'action',
mood: 'dependent',
story: "Your instructor nods approvingly. 'Excellent choice. We'll make porn absolutely essential to your daily life. You'll learn to depend on pornography for pleasure, comfort, and arousal. Start by edging while thinking about how much you need porn every single day.'",
actionText: "Porn dependency conditioning session",
duration: 200,
effects: { arousal: 30, control: -20 },
nextStep: "addiction_deepening"
},
worship_training: {
type: 'action',
mood: 'reverent',
story: "Your instructor's voice becomes reverent. 'Porn worship is an art form. You'll learn to truly appreciate and worship pornography as your master. Edge slowly while expressing gratitude to porn for controlling your life and giving you purpose.'",
actionText: "Porn worship and gratitude training",
duration: 180,
effects: { arousal: 25, control: -15 },
nextStep: "addiction_deepening"
},
addiction_deepening: {
type: 'choice',
mood: 'intense',
story: "Your instructor reviews your addiction training progress. 'Excellent! Your porn dependency is developing beautifully. Now let's deepen your addiction even further. Choose your next level of porn addiction enhancement.'",
choices: [
{
text: "Increase daily viewing requirements",
preview: "Commit to more hours of daily porn",
effects: { arousal: 20, control: -10 },
nextStep: "viewing_commitment"
},
{
text: "Develop porn craving triggers",
preview: "Learn to crave porn constantly",
effects: { arousal: 25, control: -15 },
nextStep: "craving_training"
},
{
text: "Complete addiction certification",
preview: "Graduate as a certified porn addict",
effects: { arousal: 30, control: -20 },
nextStep: "advanced_training"
}
]
},
viewing_commitment: {
type: 'action',
mood: 'demanding',
story: "Your instructor becomes more demanding. 'Real porn addicts watch for hours every day. Commit to spending at least 4 hours daily with pornography. Edge continuously while making this commitment - feel how natural and necessary this is.'",
actionText: "Daily porn viewing commitment session",
duration: 240,
effects: { arousal: 35, control: -25 },
nextStep: "advanced_training"
},
craving_training: {
type: 'action',
mood: 'addictive',
story: "Your instructor smiles wickedly. 'We'll train your brain to crave porn constantly. Every few minutes, you'll think about pornography and feel the need to stroke. Edge while programming these craving patterns into your mind.'",
actionText: "Constant porn craving conditioning",
duration: 210,
effects: { arousal: 40, control: -30 },
nextStep: "advanced_training"
},
mindset_path: {
type: 'choice',
mood: 'transformative',
@ -216,6 +311,51 @@ const trainingGameData = {
}
]
},
rhythm_training: {
type: 'action',
mood: 'rhythmic',
story: "Your instructor demonstrates complex stroking rhythms. 'Master gooners understand rhythm is everything. You'll learn slow builds, fast patterns, and variable speeds that maximize pleasure. Practice these patterns while edging to perfect porn.'",
actionText: "Multi-rhythm stroking pattern training",
duration: 180,
effects: { arousal: 25, control: 20 },
nextStep: "advanced_training"
},
pleasure_training: {
type: 'action',
mood: 'euphoric',
story: "Your instructor's voice becomes hypnotic. 'We'll teach you to extract maximum pleasure from every stroke. Learn to savor each sensation, multiply your arousal, and experience gooning bliss like never before. Edge with perfect technique.'",
actionText: "Extended pleasure maximization training",
duration: 200,
effects: { arousal: 35, control: 10 },
nextStep: "advanced_training"
},
control_training: {
type: 'action',
mood: 'precise',
story: "Your instructor becomes serious. 'Control is what separates gooners from casual masturbators. You'll learn to edge precisely at the perfect point, maintaining arousal for hours without accident. Master this and you master gooning.'",
actionText: "Advanced edge control mastery",
duration: 240,
effects: { arousal: 30, control: 35 },
nextStep: "advanced_training"
},
identity_training: {
type: 'action',
mood: 'transformative',
story: "Your instructor speaks with pride. 'Embrace your identity as a dedicated gooner. This isn't just what you do - this is who you are. Edge while accepting your true nature as someone who lives to worship porn and stroke constantly.'",
actionText: "Gooner identity acceptance training",
duration: 180,
effects: { arousal: 30, control: -15 },
nextStep: "advanced_training"
},
lifestyle_training: {
type: 'action',
mood: 'practical',
story: "Your instructor becomes practical. 'Real gooners integrate their lifestyle completely. You'll learn to schedule daily gooning time, organize your porn collection, and make gooning a central part of your routine. Edge while planning your gooner lifestyle.'",
actionText: "Gooner lifestyle integration training",
duration: 160,
effects: { arousal: 25, control: -10 },
nextStep: "advanced_training"
},
endurance_mastery: {
type: 'action',
mood: 'accomplished',

View File

@ -1042,7 +1042,7 @@ class InteractiveTaskManager {
async createFocusTask(task, container) {
console.log('🧘 Creating focus task');
// Create focus task interface
// Create focus task interface with integrated video player
container.innerHTML = `
<div class="focus-task">
<div class="task-header">
@ -1053,6 +1053,25 @@ class InteractiveTaskManager {
<div class="focus-instructions">
${task.instructions || 'Hold the position and focus for the required time'}
</div>
<div class="focus-video-container" id="focus-video-container" style="display: none;">
<video id="focus-video-player"
autoplay
style="width: 100%; max-height: 400px; border-radius: 8px; margin: 15px 0;">
</video>
<div class="video-controls" style="display: flex; align-items: center; justify-content: center; gap: 10px; margin: 10px 0;">
<label for="focus-video-volume" style="color: #bbb; font-size: 14px;">🔊</label>
<input type="range"
id="focus-video-volume"
min="0"
max="100"
value="50"
style="width: 200px; accent-color: #673ab7;">
<span id="focus-volume-display" style="color: #bbb; font-size: 12px; min-width: 35px;">50%</span>
</div>
<div class="video-info" id="video-info" style="font-size: 12px; color: #bbb; margin-top: 5px; text-align: center;">
Loading video...
</div>
</div>
<div class="focus-timer-display" id="focus-timer-display">
${task.duration || 60} seconds
</div>
@ -1118,6 +1137,22 @@ class InteractiveTaskManager {
margin: 10px 0;
`;
// Style the volume slider
const volumeSlider = container.querySelector('#focus-video-volume');
if (volumeSlider) {
volumeSlider.style.cssText = `
width: 200px;
height: 6px;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
outline: none;
cursor: pointer;
`;
}
// Initialize video management for focus session
this.initializeFocusVideos();
// Add focus session logic
const duration = task.duration || 60;
@ -1128,6 +1163,14 @@ class InteractiveTaskManager {
startBtn.style.display = 'none';
progressContainer.style.display = 'block';
// Show and start video player
const videoContainer = container.querySelector('#focus-video-container');
if (videoContainer) {
videoContainer.style.display = 'block';
this.setupFocusVolumeControl();
this.startFocusVideoPlayback();
}
let timeLeft = duration;
const progressText = container.querySelector('#progress-text');
@ -1153,6 +1196,9 @@ class InteractiveTaskManager {
progressBar.style.width = '100%';
progressBar.style.background = '#4caf50';
// Stop video playback
this.stopFocusVideoPlayback();
console.log('🧘 Focus session completed');
// Enable completion controls
@ -1166,6 +1212,341 @@ class InteractiveTaskManager {
console.log('🧘 Focus task created successfully');
}
/**
* Setup volume control for focus videos
*/
setupFocusVolumeControl() {
const volumeSlider = document.getElementById('focus-video-volume');
const volumeDisplay = document.getElementById('focus-volume-display');
if (!volumeSlider || !volumeDisplay) {
console.warn('🧘 ⚠️ Volume controls not found');
return;
}
// Get initial volume from video manager settings or default
const initialVolume = (window.videoPlayerManager?.settings?.volume || 0.5) * 100;
volumeSlider.value = initialVolume;
volumeDisplay.textContent = `${Math.round(initialVolume)}%`;
// Handle volume changes
volumeSlider.addEventListener('input', (e) => {
const volume = parseInt(e.target.value);
const volumeDecimal = volume / 100;
// Update display
volumeDisplay.textContent = `${volume}%`;
// Update video player volume
const videoPlayer = document.getElementById('focus-video-player');
if (videoPlayer) {
videoPlayer.volume = volumeDecimal;
console.log(`🧘 🔊 Volume changed to: ${volume}%`);
}
// Save to video manager settings if available
if (window.videoPlayerManager && window.videoPlayerManager.settings) {
window.videoPlayerManager.settings.volume = volumeDecimal;
console.log(`🧘 💾 Saved volume setting: ${volume}%`);
}
});
console.log(`🧘 🎛️ Volume control setup complete, initial volume: ${Math.round(initialVolume)}%`);
}
/**
* Initialize video system for focus sessions
*/
initializeFocusVideos() {
this.focusVideoLibrary = [];
this.focusVideoPlayer = null;
this.focusVideoIndex = 0;
this.focusVideoActive = false;
// Get all available videos from video manager
if (window.videoPlayerManager && window.videoPlayerManager.videoLibrary) {
const library = window.videoPlayerManager.videoLibrary;
// Combine all video categories into one pool
this.focusVideoLibrary = [
...(library.task || []),
...(library.reward || []),
...(library.punishment || []),
...(library.background || [])
];
console.log(`🧘 🎬 Initialized focus video library with ${this.focusVideoLibrary.length} videos`);
if (this.focusVideoLibrary.length === 0) {
console.warn('🧘 ⚠️ No videos found in any category, focus videos will be disabled');
}
} else {
console.warn('🧘 ⚠️ Video manager not available, focus videos disabled');
}
}
/**
* Start continuous video playback for focus session
*/
async startFocusVideoPlayback() {
if (this.focusVideoLibrary.length === 0) {
console.warn('🧘 ⚠️ No videos available for focus session, hiding video container');
const videoContainer = document.getElementById('focus-video-container');
if (videoContainer) {
videoContainer.style.display = 'none';
}
return;
}
this.focusVideoActive = true;
this.focusVideoPlayer = document.getElementById('focus-video-player');
if (!this.focusVideoPlayer) {
console.error('🧘 ❌ Focus video player element not found');
return;
}
// Set up video ended event to play next video automatically
this.focusVideoPlayer.addEventListener('ended', () => {
if (this.focusVideoActive) {
console.log('🧘 🎬 Video ended, playing next...');
setTimeout(() => this.playNextFocusVideo(), 500); // Small delay before next video
} else {
console.log('🧘 📹 Video ended after session completed, not playing next');
}
});
// Set up error handling
this.focusVideoPlayer.addEventListener('error', (e) => {
// Don't handle errors if the focus session is no longer active
if (!this.focusVideoActive) {
console.log('🧘 📹 Video error after session ended, ignoring');
return;
}
const error = this.focusVideoPlayer.error;
let errorMessage = 'Unknown video error';
if (error) {
switch (error.code) {
case 1: errorMessage = 'Video loading aborted'; break;
case 2: errorMessage = 'Network error'; break;
case 3: errorMessage = 'Video decoding error'; break;
case 4: errorMessage = 'Video format not supported'; break;
default: errorMessage = `Video error code: ${error.code}`;
}
}
console.warn(`🧘 ⚠️ Video error (${errorMessage}), skipping to next video`);
if (this.focusVideoActive) {
// Update video info to show error
const videoInfo = document.getElementById('video-info');
if (videoInfo) {
videoInfo.textContent = `Error: ${errorMessage}, loading next...`;
}
setTimeout(() => this.playNextFocusVideo(), 1000);
}
});
// Set up loading start
this.focusVideoPlayer.addEventListener('loadstart', () => {
const videoInfo = document.getElementById('video-info');
if (videoInfo) {
videoInfo.textContent = 'Loading video...';
}
});
// Start playing the first video
this.playNextFocusVideo();
}
/**
* Check if video format is supported
*/
isVideoFormatSupported(videoPath) {
const extension = videoPath.toLowerCase().split('.').pop();
const supportedFormats = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'mkv'];
// Check basic extension support
if (!supportedFormats.includes(extension)) {
console.warn(`🧘 ⚠️ Potentially unsupported video format: .${extension}`);
return false;
}
// For .mov files, check if browser can play them
if (extension === 'mov') {
const testVideo = document.createElement('video');
const canPlay = testVideo.canPlayType('video/quicktime');
if (canPlay === '') {
console.warn('🧘 ⚠️ Browser may not support .mov files');
return false;
}
}
return true;
}
/**
* Play the next random video in the focus session
*/
async playNextFocusVideo() {
if (!this.focusVideoActive || this.focusVideoLibrary.length === 0) {
return;
}
// Try up to 5 times to find a compatible video
let attempts = 0;
let videoFile, videoPath;
do {
// Pick a random video
const randomIndex = Math.floor(Math.random() * this.focusVideoLibrary.length);
videoFile = this.focusVideoLibrary[randomIndex];
// Get video path (handle both string and object formats)
if (typeof videoFile === 'string') {
videoPath = videoFile;
} else if (videoFile.path) {
videoPath = videoFile.path;
} else if (videoFile.name) {
videoPath = videoFile.name;
} else {
console.warn('🧘 ⚠️ Invalid video file format:', videoFile);
attempts++;
continue;
}
// Check if format is supported
if (this.isVideoFormatSupported(videoPath)) {
break; // Found a compatible video
}
attempts++;
} while (attempts < 5);
if (attempts >= 5) {
console.warn('🧘 ⚠️ Could not find compatible video after 5 attempts');
const videoInfo = document.getElementById('video-info');
if (videoInfo) {
videoInfo.textContent = 'No compatible videos found';
}
return;
}
try {
// Construct full path
const fullPath = this.getVideoFilePath(videoPath);
console.log(`🧘 🎬 Playing focus video: ${videoPath}`);
// Update video info display
const videoInfo = document.getElementById('video-info');
if (videoInfo) {
const fileName = videoPath.split('/').pop().split('\\').pop();
videoInfo.textContent = `Playing: ${fileName}`;
}
// Set video source and play
this.focusVideoPlayer.src = fullPath;
this.focusVideoPlayer.muted = false; // Ensure video is not muted
// Get volume from slider if available, otherwise use default
const volumeSlider = document.getElementById('focus-video-volume');
let volume = 0.5; // default
if (volumeSlider) {
volume = parseInt(volumeSlider.value) / 100;
} else {
volume = window.videoPlayerManager?.settings?.volume || 0.5;
}
this.focusVideoPlayer.volume = volume;
console.log(`🧘 🎬 Video volume set to: ${Math.round(volume * 100)}%`);
// Add load event to handle successful loading
const handleLoad = () => {
const fileName = videoPath.split('/').pop().split('\\').pop();
console.log(`🧘 🎬 Video loaded successfully: ${fileName}`);
this.focusVideoPlayer.removeEventListener('loadeddata', handleLoad);
};
this.focusVideoPlayer.addEventListener('loadeddata', handleLoad);
try {
await this.focusVideoPlayer.play();
const fileName = videoPath.split('/').pop().split('\\').pop();
console.log(`🧘 🎬 Video playing: ${fileName}`);
} catch (playError) {
console.warn('🧘 ⚠️ Could not autoplay video:', playError);
// Still might work with user interaction, so don't skip immediately
}
} catch (error) {
console.error('🧘 ❌ Error setting up focus video:', error);
// Try another video after a short delay
setTimeout(() => {
if (this.focusVideoActive) {
this.playNextFocusVideo();
}
}, 1000);
}
}
/**
* Stop focus video playback
*/
stopFocusVideoPlayback() {
console.log('🧘 🎬 Stopping focus video playback...');
this.focusVideoActive = false;
if (this.focusVideoPlayer) {
// Pause and clear the video
this.focusVideoPlayer.pause();
this.focusVideoPlayer.src = '';
this.focusVideoPlayer.load(); // Reset the video element
// Update video info
const videoInfo = document.getElementById('video-info');
if (videoInfo) {
videoInfo.textContent = 'Video session completed';
}
// Hide video container
const videoContainer = document.getElementById('focus-video-container');
if (videoContainer) {
videoContainer.style.display = 'none';
}
}
console.log('🧘 🎬 Focus video playback stopped');
}
/**
* Get proper file path for videos
*/
getVideoFilePath(videoPath) {
// If it's already a full path or data URL, use as-is
if (videoPath.startsWith('http') || videoPath.startsWith('data:') || videoPath.startsWith('file:')) {
return videoPath;
}
// For desktop app, construct proper file path
if (window.electronAPI) {
// Handle both forward and back slashes and ensure proper file:// protocol
const normalizedPath = videoPath.replace(/\\/g, '/');
// Remove any leading slashes to avoid file:////
const cleanPath = normalizedPath.replace(/^\/+/, '');
return `file:///${cleanPath}`;
}
// For web version, assume relative path in videos directory
if (!videoPath.startsWith('videos/')) {
return `videos/${videoPath}`;
}
return videoPath;
}
async validateFocusTask(task) {
const timerDisplay = document.getElementById('focus-timer-display');
const progressText = document.getElementById('progress-text');
@ -1395,7 +1776,7 @@ class InteractiveTaskManager {
const endingDiv = document.createElement('div');
endingDiv.className = 'scenario-ending';
endingDiv.innerHTML = `
<div class="ending-text">${step.endingText}</div>
<div class="ending-text">${this.processScenarioText(step.endingText, task.scenarioState)}</div>
<div class="ending-outcome ${step.outcome}">${this.getOutcomeText(step.outcome)}</div>
`;

View File

@ -0,0 +1,529 @@
/**
* Video Player Manager
* Handles video playback, management, and integration with game modes
*/
class VideoPlayerManager {
constructor() {
this.currentPlayer = null;
this.backgroundPlayer = null;
this.overlayPlayer = null;
this.videoLibrary = {
background: [],
task: [],
reward: [],
punishment: []
};
this.settings = {
enabled: true,
backgroundVideos: true,
taskVideos: true,
autoplay: true,
loop: true,
volume: 0.3,
muted: false,
playbackRate: 1.0,
showControls: false,
fadeTransitions: true
};
this.isInitialized = false;
this.loadSettings();
}
/**
* Initialize the video player system
*/
async init() {
console.log('🎬 Initializing Video Player Manager...');
try {
this.createVideoPlayers();
this.bindEvents();
await this.scanVideoLibrary();
this.isInitialized = true;
console.log('✅ Video Player Manager initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize Video Player Manager:', error);
}
}
/**
* Create video player elements
*/
createVideoPlayers() {
// Background video player (behind content)
this.backgroundPlayer = this.createVideoElement('background-video-player', {
loop: true,
muted: true,
autoplay: true,
style: `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
object-fit: cover;
z-index: -1;
opacity: 0.3;
pointer-events: none;
`
});
// Task video player (in content area)
this.currentPlayer = this.createVideoElement('task-video-player', {
controls: this.settings.showControls,
style: `
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
display: none;
`
});
// Overlay video player (for popups/punishments)
this.overlayPlayer = this.createVideoElement('overlay-video-player', {
autoplay: true,
style: `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 80vw;
max-height: 80vh;
z-index: 10000;
border-radius: 8px;
box-shadow: 0 0 50px rgba(0,0,0,0.8);
display: none;
`
});
// Add players to DOM
document.body.appendChild(this.backgroundPlayer);
document.body.appendChild(this.overlayPlayer);
// Add task player to game container
const gameContainer = document.querySelector('.game-container') || document.body;
gameContainer.appendChild(this.currentPlayer);
}
/**
* Create a video element with specified attributes
*/
createVideoElement(id, attributes = {}) {
const video = document.createElement('video');
video.id = id;
// Set default attributes
video.preload = 'metadata';
video.playsInline = true;
video.webkit = { playsinline: true };
// Apply custom attributes
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'style') {
video.style.cssText = value;
} else {
video[key] = value;
}
});
// Add event listeners
video.addEventListener('loadstart', () => console.log(`🎬 Loading video: ${video.src}`));
video.addEventListener('canplay', () => console.log(`✅ Video ready: ${video.src}`));
video.addEventListener('error', (e) => console.error(`❌ Video error: ${e.message}`));
video.addEventListener('ended', (e) => this.handleVideoEnd(e));
return video;
}
/**
* Scan video directory for available videos
*/
async scanVideoLibrary() {
console.log('📁 Scanning video library...');
const categories = ['background', 'task', 'reward', 'punishment'];
for (const category of categories) {
try {
// In Electron environment, use the desktop file manager
if (window.electronAPI && window.desktopFileManager) {
// Map category names to match file manager
const dirCategory = category === 'task' ? 'tasks' :
category === 'reward' ? 'rewards' :
category === 'punishment' ? 'punishments' : category;
const videos = await window.desktopFileManager.scanDirectoryForVideos(dirCategory);
this.videoLibrary[category] = videos;
} else {
// In browser environment, try to load from localStorage
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
const dirCategory = category === 'task' ? 'tasks' :
category === 'reward' ? 'rewards' :
category === 'punishment' ? 'punishments' : category;
this.videoLibrary[category] = storedVideos[dirCategory] || [];
}
console.log(`📹 Found ${this.videoLibrary[category].length} ${category} videos`);
} catch (error) {
console.warn(`⚠️ Could not scan ${category} videos:`, error);
this.videoLibrary[category] = [];
}
}
}
/**
* Get known videos for browser environment (fallback)
*/
getKnownVideos(category) {
// This would need to be populated manually in browser environment
// or through a manifest file
return [];
}
/**
* Reload video files from storage (called when new videos are imported)
*/
async loadVideoFiles() {
console.log('🔄 Reloading video library...');
await this.scanVideoLibrary();
}
/**
* Play background video
*/
async playBackgroundVideo(videoPath = null) {
if (!this.settings.backgroundVideos || !this.backgroundPlayer) return;
try {
if (!videoPath) {
const backgroundVideos = this.videoLibrary.background;
if (backgroundVideos.length === 0) return;
videoPath = backgroundVideos[Math.floor(Math.random() * backgroundVideos.length)];
}
const fullPath = this.getVideoPath(videoPath);
console.log(`🎬 Playing background video: ${fullPath}`);
if (this.settings.fadeTransitions) {
await this.fadeOut(this.backgroundPlayer);
}
this.backgroundPlayer.src = fullPath;
this.backgroundPlayer.volume = this.settings.volume * 0.5; // Background videos quieter
this.backgroundPlayer.muted = this.settings.muted;
this.backgroundPlayer.playbackRate = this.settings.playbackRate;
await this.backgroundPlayer.play();
if (this.settings.fadeTransitions) {
await this.fadeIn(this.backgroundPlayer);
}
} catch (error) {
console.error('❌ Failed to play background video:', error);
}
}
/**
* Play task video
*/
async playTaskVideo(videoPath, container = null) {
if (!this.settings.taskVideos || !this.currentPlayer) return;
try {
const fullPath = this.getVideoPath(videoPath);
console.log(`🎬 Playing task video: ${fullPath}`);
// Position player in specified container
if (container) {
container.appendChild(this.currentPlayer);
}
this.currentPlayer.src = fullPath;
this.currentPlayer.volume = this.settings.volume;
this.currentPlayer.muted = this.settings.muted;
this.currentPlayer.controls = this.settings.showControls;
this.currentPlayer.style.display = 'block';
if (this.settings.autoplay) {
await this.currentPlayer.play();
}
if (this.settings.fadeTransitions) {
await this.fadeIn(this.currentPlayer);
}
} catch (error) {
console.error('❌ Failed to play task video:', error);
}
}
/**
* Play overlay video (for punishments/rewards)
*/
async playOverlayVideo(videoPath, options = {}) {
if (!this.overlayPlayer) return;
try {
const fullPath = this.getVideoPath(videoPath);
console.log(`🎬 Playing overlay video: ${fullPath}`);
const {
duration = null,
closeable = false,
onComplete = null,
volume = this.settings.volume
} = options;
this.overlayPlayer.src = fullPath;
this.overlayPlayer.volume = volume;
this.overlayPlayer.muted = this.settings.muted;
this.overlayPlayer.style.display = 'block';
// Add close button if closeable
if (closeable) {
this.addCloseButton(this.overlayPlayer);
}
await this.overlayPlayer.play();
if (this.settings.fadeTransitions) {
await this.fadeIn(this.overlayPlayer);
}
// Auto-close after duration
if (duration) {
setTimeout(() => {
this.closeOverlayVideo();
if (onComplete) onComplete();
}, duration);
}
} catch (error) {
console.error('❌ Failed to play overlay video:', error);
}
}
/**
* Close overlay video
*/
async closeOverlayVideo() {
if (!this.overlayPlayer) return;
if (this.settings.fadeTransitions) {
await this.fadeOut(this.overlayPlayer);
}
this.overlayPlayer.pause();
this.overlayPlayer.style.display = 'none';
this.overlayPlayer.src = '';
// Remove close button if exists
const closeBtn = this.overlayPlayer.nextElementSibling;
if (closeBtn && closeBtn.classList.contains('video-close-btn')) {
closeBtn.remove();
}
}
/**
* Stop all videos
*/
stopAllVideos() {
[this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => {
if (player) {
player.pause();
player.currentTime = 0;
}
});
}
/**
* Pause all videos
*/
pauseAllVideos() {
[this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => {
if (player && !player.paused) {
player.pause();
}
});
}
/**
* Resume all videos
*/
resumeAllVideos() {
[this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => {
if (player && player.paused) {
player.play().catch(console.error);
}
});
}
/**
* Get random video from category
*/
getRandomVideo(category) {
const videos = this.videoLibrary[category] || [];
if (videos.length === 0) return null;
return videos[Math.floor(Math.random() * videos.length)];
}
/**
* Get full video path
*/
getVideoPath(videoPath) {
if (videoPath.startsWith('http') || videoPath.startsWith('file:') || videoPath.startsWith('video/')) {
return videoPath;
}
return `video/${videoPath}`;
}
/**
* Add close button to video
*/
addCloseButton(videoElement) {
const closeBtn = document.createElement('button');
closeBtn.className = 'video-close-btn';
closeBtn.innerHTML = '✕';
closeBtn.style.cssText = `
position: fixed;
top: calc(${videoElement.style.top} - 30px);
right: calc(50% - ${videoElement.offsetWidth/2}px + ${videoElement.offsetWidth}px - 30px);
z-index: 10001;
background: rgba(0,0,0,0.8);
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.addEventListener('click', () => this.closeOverlayVideo());
videoElement.parentNode.insertBefore(closeBtn, videoElement.nextSibling);
}
/**
* Fade in animation
*/
fadeIn(element, duration = 500) {
return new Promise(resolve => {
element.style.opacity = '0';
element.style.transition = `opacity ${duration}ms ease-in-out`;
requestAnimationFrame(() => {
element.style.opacity = element.id === 'background-video-player' ? '0.3' : '1';
setTimeout(resolve, duration);
});
});
}
/**
* Fade out animation
*/
fadeOut(element, duration = 500) {
return new Promise(resolve => {
element.style.transition = `opacity ${duration}ms ease-in-out`;
element.style.opacity = '0';
setTimeout(resolve, duration);
});
}
/**
* Handle video end events
*/
handleVideoEnd(event) {
const video = event.target;
if (video === this.backgroundPlayer && this.settings.loop) {
// Start next background video
this.playBackgroundVideo();
} else if (video === this.overlayPlayer) {
// Auto-close overlay videos
this.closeOverlayVideo();
}
}
/**
* Update settings
*/
updateSettings(newSettings) {
this.settings = { ...this.settings, ...newSettings };
this.saveSettings();
this.applySettings();
}
/**
* Apply current settings to players
*/
applySettings() {
[this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => {
if (player) {
player.volume = this.settings.volume;
player.muted = this.settings.muted;
player.playbackRate = this.settings.playbackRate;
if (player === this.currentPlayer) {
player.controls = this.settings.showControls;
}
}
});
}
/**
* Load settings from localStorage
*/
loadSettings() {
const saved = localStorage.getItem('videoPlayerSettings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
}
/**
* Save settings to localStorage
*/
saveSettings() {
localStorage.setItem('videoPlayerSettings', JSON.stringify(this.settings));
}
/**
* Bind event listeners
*/
bindEvents() {
// Game pause/resume integration
document.addEventListener('gamepaused', () => this.pauseAllVideos());
document.addEventListener('gameresumed', () => this.resumeAllVideos());
// Cleanup on page unload
window.addEventListener('beforeunload', () => this.stopAllVideos());
}
/**
* Get video library info
*/
getLibraryInfo() {
return {
background: this.videoLibrary.background.length,
task: this.videoLibrary.task.length,
reward: this.videoLibrary.reward.length,
punishment: this.videoLibrary.punishment.length,
total: Object.values(this.videoLibrary).flat().length
};
}
}
// Create global instance
window.videoPlayerManager = new VideoPlayerManager();
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
if (window.videoPlayerManager) {
window.videoPlayerManager.init();
}
});
console.log('🎬 Video Player Manager loaded');

View File

@ -877,22 +877,155 @@ class WebcamManager {
}
/**
* Download all photos as a zip (simplified version - individual downloads)
* Download selected photos - single photo normally, multiple photos as zip
*/
downloadAllPhotos() {
async downloadSelectedPhotos(selectedPhotos) {
if (!selectedPhotos || selectedPhotos.length === 0) {
this.showNotification('No photos selected for download', 'warning');
return;
}
if (selectedPhotos.length === 1) {
// Single photo - download normally
const photo = selectedPhotos[0];
const filename = `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
const success = this.downloadPhoto(photo, filename);
if (success) {
this.showNotification(`✅ Photo downloaded as ${filename}`, 'success');
console.log(`📸 Single photo download completed: ${filename}`);
}
} else {
// Multiple photos - create zip
await this.downloadPhotosAsZip(selectedPhotos);
}
}
/**
* Convert data URL to blob without using fetch (CSP-safe)
*/
dataURLToBlob(dataURL) {
try {
const [header, data] = dataURL.split(',');
const mime = header.match(/:(.*?);/)[1];
const binary = atob(data);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return new Blob([array], { type: mime });
} catch (error) {
console.error('Failed to convert data URL to blob:', error);
return null;
}
}
/**
* Download photos as a zip file
*/
async downloadPhotosAsZip(photos, zipFilename = null) {
// Wait for JSZip to be available
let JSZipLibrary = null;
let attempts = 0;
const maxAttempts = 50; // 5 seconds max wait time
while (!JSZipLibrary && attempts < maxAttempts) {
if (typeof JSZip !== 'undefined') {
JSZipLibrary = JSZip;
break;
} else if (typeof window.JSZip !== 'undefined') {
JSZipLibrary = window.JSZip;
break;
}
// Wait 100ms before trying again
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
// Check if JSZip is available after waiting
if (!JSZipLibrary) {
console.error('JSZip library not available after waiting. Please ensure JSZip is loaded.');
this.showNotification('❌ Zip functionality not available. JSZip library failed to load.', 'error');
return;
}
try {
this.showNotification(`📦 Creating zip with ${photos.length} photos...`, 'info');
const zip = new JSZipLibrary();
// Add each photo to the zip
for (let i = 0; i < photos.length; i++) {
const photo = photos[i];
const timestamp = new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-');
const filename = `photo_${i + 1}_${timestamp}.jpg`;
// Convert data URL to blob using helper function
const blob = this.dataURLToBlob(photo.dataURL);
if (!blob) {
console.warn(`Failed to convert photo ${i + 1} to blob, skipping`);
continue;
}
zip.file(filename, blob);
}
// Generate the zip file
const content = await zip.generateAsync({type: 'blob'});
// Create unique filename with date/time if not provided
let finalFilename = zipFilename;
if (!finalFilename) {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '-'); // HH-MM-SS
finalFilename = `photos_${dateStr}_${timeStr}.zip`;
}
// Download the zip
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = finalFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the object URL
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
// Show success confirmation with filename
this.showNotification(`✅ Downloaded ${photos.length} photos as ${finalFilename}`, 'success');
console.log(`📦 Zip download completed: ${finalFilename} (${photos.length} photos)`);
} catch (error) {
console.error('Failed to create zip:', error);
this.showNotification('❌ Failed to create zip file', 'error');
}
}
/**
* Download all photos as a zip file
*/
async 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');
if (photos.length === 1) {
// Single photo - download normally
const photo = photos[0];
const filename = `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
const success = this.downloadPhoto(photo, filename);
if (success) {
this.showNotification(`✅ Photo downloaded as ${filename}`, 'success');
console.log(`📸 Single photo download completed: ${filename}`);
}
} else {
// Multiple photos - create zip (filename will be auto-generated with timestamp)
await this.downloadPhotosAsZip(photos);
}
}
/**

View File

@ -12,6 +12,12 @@ class DesktopFileManager {
ambient: null,
effects: null
};
this.videoDirectories = {
background: null,
tasks: null,
rewards: null,
punishments: null
};
this.init();
}
@ -31,6 +37,11 @@ class DesktopFileManager {
this.audioDirectories.ambient = await window.electronAPI.pathJoin(this.appPath, 'audio', 'ambient');
this.audioDirectories.effects = await window.electronAPI.pathJoin(this.appPath, 'audio', 'effects');
this.videoDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'videos', 'background');
this.videoDirectories.tasks = await window.electronAPI.pathJoin(this.appPath, 'videos', 'tasks');
this.videoDirectories.rewards = await window.electronAPI.pathJoin(this.appPath, 'videos', 'rewards');
this.videoDirectories.punishments = await window.electronAPI.pathJoin(this.appPath, 'videos', 'punishments');
// Ensure directories exist
await window.electronAPI.createDirectory(this.imageDirectories.tasks);
await window.electronAPI.createDirectory(this.imageDirectories.consequences);
@ -39,6 +50,11 @@ class DesktopFileManager {
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
await window.electronAPI.createDirectory(this.audioDirectories.effects);
await window.electronAPI.createDirectory(this.videoDirectories.background);
await window.electronAPI.createDirectory(this.videoDirectories.tasks);
await window.electronAPI.createDirectory(this.videoDirectories.rewards);
await window.electronAPI.createDirectory(this.videoDirectories.punishments);
console.log('Desktop file manager initialized');
console.log('App path:', this.appPath);
console.log('Image directories:', this.imageDirectories);
@ -184,6 +200,85 @@ class DesktopFileManager {
}
}
async selectAndImportVideos(category = 'background') {
if (!this.isElectron) {
this.showNotification('Video import only available in desktop version', 'warning');
return [];
}
try {
// Open file dialog for video files
const filePaths = await window.electronAPI.selectVideos();
if (filePaths.length === 0) {
return [];
}
const importedVideos = [];
let targetDir;
switch(category) {
case 'background':
targetDir = this.videoDirectories.background;
break;
case 'task':
case 'tasks':
targetDir = this.videoDirectories.tasks;
break;
case 'reward':
case 'rewards':
targetDir = this.videoDirectories.rewards;
break;
case 'punishment':
case 'punishments':
targetDir = this.videoDirectories.punishments;
break;
default:
targetDir = this.videoDirectories.background;
}
if (!targetDir) {
console.error('Target video directory not initialized');
this.showNotification('Video directory not initialized', 'error');
return [];
}
for (const filePath of filePaths) {
const fileName = filePath.split(/[\\/]/).pop();
const targetPath = await window.electronAPI.pathJoin(targetDir, fileName);
// Copy file to app directory
const success = await window.electronAPI.copyVideo(filePath, targetPath);
if (success) {
importedVideos.push({
name: fileName,
path: targetPath,
category: category,
title: this.getVideoTitle(fileName),
url: `file:///${targetPath.replace(/\\/g, '/')}`
});
console.log(`Imported video: ${fileName} to ${category}`);
} else {
console.error(`Failed to import video: ${fileName}`);
}
}
if (importedVideos.length > 0) {
// Update the game's video storage
await this.updateVideoStorage(importedVideos);
this.showNotification(`Imported ${importedVideos.length} video file(s) to ${category}!`, 'success');
}
return importedVideos;
} catch (error) {
console.error('Error importing videos:', error);
this.showNotification('Failed to import video files', 'error');
return [];
}
}
async scanDirectoryForImages(category = 'task') {
if (!this.isElectron) {
return [];
@ -214,24 +309,44 @@ class DesktopFileManager {
async scanAllDirectories() {
if (!this.isElectron) {
this.showNotification('Directory scanning only available in desktop version', 'warning');
return { task: [], consequence: [] };
return { task: [], consequence: [], videos: { background: [], tasks: [], rewards: [], punishments: [] } };
}
// Scan image directories
const taskImages = await this.scanDirectoryForImages('task');
const consequenceImages = await this.scanDirectoryForImages('consequence');
// Scan video directories
const backgroundVideos = await this.scanDirectoryForVideos('background');
const taskVideos = await this.scanDirectoryForVideos('tasks');
const rewardVideos = await this.scanDirectoryForVideos('rewards');
const punishmentVideos = await this.scanDirectoryForVideos('punishments');
const results = {
task: taskImages,
consequence: consequenceImages
consequence: consequenceImages,
videos: {
background: backgroundVideos,
tasks: taskVideos,
rewards: rewardVideos,
punishments: punishmentVideos
}
};
// Update game storage
// Update image storage
if (taskImages.length > 0 || consequenceImages.length > 0) {
await this.updateImageStorage([...taskImages, ...consequenceImages]);
}
const totalFound = taskImages.length + consequenceImages.length;
this.showNotification(`Found ${totalFound} images (${taskImages.length} tasks, ${consequenceImages.length} consequences)`, 'success');
// Update video storage
const allVideos = [...backgroundVideos, ...taskVideos, ...rewardVideos, ...punishmentVideos];
if (allVideos.length > 0) {
await this.updateVideoStorage(allVideos);
}
const totalImages = taskImages.length + consequenceImages.length;
const totalVideos = allVideos.length;
this.showNotification(`Found ${totalImages} images (${taskImages.length} tasks, ${consequenceImages.length} consequences) and ${totalVideos} videos (${backgroundVideos.length} background, ${taskVideos.length} tasks, ${rewardVideos.length} rewards, ${punishmentVideos.length} punishments)`, 'success');
return results;
}
@ -517,6 +632,132 @@ class DesktopFileManager {
const dir = this.imageDirectories[category === 'task' ? 'tasks' : 'consequences'];
return `${dir}/${imageName}`;
}
getVideoTitle(fileName) {
// Remove file extension and clean up the name for display
return fileName
.replace(/\.[^/.]+$/, '') // Remove extension
.replace(/[-_]/g, ' ') // Replace dashes and underscores with spaces
.replace(/\b\w/g, l => l.toUpperCase()); // Capitalize first letters
}
getVideoPath(videoName, category = 'background') {
if (!this.isElectron) {
return `videos/${videoName}`;
}
let dir;
switch(category) {
case 'background':
dir = this.videoDirectories.background;
break;
case 'tasks':
dir = this.videoDirectories.tasks;
break;
case 'rewards':
dir = this.videoDirectories.rewards;
break;
case 'punishments':
dir = this.videoDirectories.punishments;
break;
default:
dir = this.videoDirectories.background;
}
return `${dir}/${videoName}`;
}
async updateVideoStorage(videoFiles) {
// Get existing videos from localStorage
const existingVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
// Add new videos
videoFiles.forEach(video => {
if (!existingVideos[video.category]) {
existingVideos[video.category] = [];
}
// Check if video already exists (prevent duplicates)
const exists = existingVideos[video.category].some(existing => existing.name === video.name);
if (!exists) {
existingVideos[video.category].push(video);
}
});
// Save back to localStorage
localStorage.setItem('videoFiles', JSON.stringify(existingVideos));
// Trigger video manager reload if it exists
if (window.videoPlayerManager) {
window.videoPlayerManager.loadVideoFiles();
}
}
async scanDirectoryForVideos(category = 'background') {
if (!this.isElectron) {
return [];
}
try {
let targetDir;
switch(category) {
case 'background':
targetDir = this.videoDirectories.background;
break;
case 'task':
case 'tasks':
targetDir = this.videoDirectories.tasks;
break;
case 'reward':
case 'rewards':
targetDir = this.videoDirectories.rewards;
break;
case 'punishment':
case 'punishments':
targetDir = this.videoDirectories.punishments;
break;
default:
targetDir = this.videoDirectories.background;
}
if (!targetDir) {
console.log(`Video directory for ${category} not initialized`);
return [];
}
const videos = await window.electronAPI.readVideoDirectory(targetDir);
// Add category information
return videos.map(video => ({
...video,
category: category
}));
} catch (error) {
console.error(`Error scanning video directory for ${category}:`, error);
return [];
}
}
async loadAllVideos() {
if (!this.isElectron) {
return;
}
try {
// Scan all video directories
const backgroundVideos = await this.scanDirectoryForVideos('background');
const taskVideos = await this.scanDirectoryForVideos('tasks');
const rewardVideos = await this.scanDirectoryForVideos('rewards');
const punishmentVideos = await this.scanDirectoryForVideos('punishments');
// Update storage with all found videos
await this.updateVideoStorage([...backgroundVideos, ...taskVideos, ...rewardVideos, ...punishmentVideos]);
} catch (error) {
console.error('Error loading videos from directories:', error);
}
}
}
// Global file manager instance