Fix Porn Cinema video loading bug and cleanup debug logs
CRITICAL BUG FIX: Porn Cinema Video Loading - Fixed destructive data overwrite issue where DesktopFileManager was clearing unified video library - Added data preservation logic to prevent overwriting existing videos when no directories are linked - Enhanced VideoLibrary fallback mechanisms for reliable video access across components - Resolved timing/synchronization issues between main game and Porn Cinema CLEANUP & OPTIMIZATION - Removed excessive debug logging from porn-cinema.html initialization - Cleaned up console output in desktop-file-manager.js and videoLibrary.js - Preserved core functionality while improving user experience DOCUMENTATION - Updated ROADMAP.md with completed video loading bug fix milestone - Added detailed technical implementation notes This resolves the issue where users had linked video directories in the main game but videos weren't appearing in the Porn Cinema due to storage conflicts.
This commit is contained in:
parent
b692260015
commit
77af6076e0
|
|
@ -1,80 +0,0 @@
|
||||||
# Simplified Audio System Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The audio system has been completely refactored to use a simple, dynamic approach with a streamlined directory structure.
|
|
||||||
|
|
||||||
## Simplified Directory Structure
|
|
||||||
|
|
||||||
### Current Structure:
|
|
||||||
```
|
|
||||||
audio/
|
|
||||||
├── background/ (Contains all background music/audio clips)
|
|
||||||
└── ambient/ (Empty for now, reserved for future ambient sounds)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Previous Complex Structure (REMOVED):
|
|
||||||
- ❌ `audio/effects/`
|
|
||||||
- ❌ `audio/tasks/teasing/`
|
|
||||||
- ❌ `audio/tasks/intense/`
|
|
||||||
- ❌ `audio/tasks/instructions/`
|
|
||||||
- ❌ `audio/punishments/denial/`
|
|
||||||
- ❌ `audio/punishments/mocking/`
|
|
||||||
- ❌ `audio/rewards/completion/`
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### Single Background Audio Category
|
|
||||||
- **Before**: Multiple hardcoded categories (tasks, punishments, rewards, teasing, intense, etc.)
|
|
||||||
- **After**: One dynamic "background" category that uses user-managed audio files
|
|
||||||
|
|
||||||
### Dynamic Audio Discovery
|
|
||||||
- Audio files are loaded from the existing `customAudio` storage system
|
|
||||||
- Combines all user audio (background, ambient, effects) into one pool
|
|
||||||
- No more hardcoded file paths that can break
|
|
||||||
- Automatically refreshes when users add new audio files
|
|
||||||
|
|
||||||
### User Experience
|
|
||||||
1. **Add Audio**: Use "Manage Audio" menu to import audio files
|
|
||||||
2. **Auto-Play**: Audio automatically plays during tasks, rewards, and consequences
|
|
||||||
3. **Single Control**: All audio settings now control the same background audio
|
|
||||||
4. **No Errors**: No more "file not found" errors from missing hardcoded files
|
|
||||||
|
|
||||||
## Technical Changes
|
|
||||||
|
|
||||||
### AudioManager Simplification
|
|
||||||
- `playTaskAudio()` → `playBackgroundAudio()`
|
|
||||||
- `playPunishmentAudio()` → `playBackgroundAudio()`
|
|
||||||
- `playRewardAudio()` → `playBackgroundAudio()`
|
|
||||||
- Single audio category instead of multiple complex categories
|
|
||||||
|
|
||||||
### Game Integration
|
|
||||||
- All game events (task start, completion, skip) play from the same audio pool
|
|
||||||
- Audio automatically refreshes when users add files via "Manage Audio"
|
|
||||||
- Settings UI still shows separate controls but they all control the same audio
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
- ✅ No more audio file path errors
|
|
||||||
- ✅ User has full control over audio content
|
|
||||||
- ✅ Simple, reliable system
|
|
||||||
- ✅ Integrates with existing UI
|
|
||||||
- ✅ No hardcoded dependencies
|
|
||||||
|
|
||||||
## For Users
|
|
||||||
|
|
||||||
### To Add Background Audio:
|
|
||||||
1. Place your audio files in the `audio/background/` directory
|
|
||||||
2. Open the main menu and click "Manage Audio"
|
|
||||||
3. Click "🔍 Scan Directories" to discover the files
|
|
||||||
4. Audio will automatically play during the game
|
|
||||||
|
|
||||||
### Audio Controls:
|
|
||||||
- All volume sliders now control the same background audio
|
|
||||||
- Enable/disable toggles all control the same background audio
|
|
||||||
- Preview buttons all preview your background audio
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
- Complex directory structure has been simplified to just two folders
|
|
||||||
- Existing user audio files in "Manage Audio" will automatically work
|
|
||||||
- All audio files should be placed in `audio/background/` for now
|
|
||||||
- `audio/ambient/` is reserved for future ambient sounds (leave empty)
|
|
||||||
- System is now completely user-controlled and dynamic
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
# Audio Directory Scanning Feature
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The directory scanning feature automatically discovers audio files that are already present in your app's audio directory structure and adds them to your audio library.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### Automatic Scanning
|
|
||||||
- **On Startup**: The app automatically scans for audio files when it launches (desktop mode only)
|
|
||||||
- **Manual Scanning**: Use the "🔍 Scan Directories" button in the "Manage Audio" menu
|
|
||||||
|
|
||||||
### Scanned Directories
|
|
||||||
The scanner looks for audio files in these directories:
|
|
||||||
```
|
|
||||||
audio/
|
|
||||||
├── background/
|
|
||||||
└── ambient/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Audio Formats
|
|
||||||
- **MP3** (most common)
|
|
||||||
- **WAV** (high quality)
|
|
||||||
- **OGG** (open source)
|
|
||||||
- **M4A** (Apple format)
|
|
||||||
- **AAC** (compressed)
|
|
||||||
- **FLAC** (lossless)
|
|
||||||
|
|
||||||
## Using Directory Scanning
|
|
||||||
|
|
||||||
### Desktop Mode (Recommended)
|
|
||||||
1. **Place Files**: Copy your audio files into the appropriate audio subdirectories
|
|
||||||
2. **Scan**: Click "🔍 Scan Directories" in the "Manage Audio" menu
|
|
||||||
3. **Auto-Use**: Scanned files are automatically added to your library and enabled
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
- **Background Music** → `audio/background/`
|
|
||||||
- **Ambient Sounds** → `audio/ambient/`
|
|
||||||
|
|
||||||
## Category Mapping
|
|
||||||
Due to the simplified audio system, all scanned files are categorized as follows:
|
|
||||||
- Files from `audio/ambient/` → **Ambient** category
|
|
||||||
- Files from `audio/background/` → **Background** category
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
### Easy Bulk Import
|
|
||||||
- Add many files at once by copying them to directories
|
|
||||||
- No need to import files one-by-one through the UI
|
|
||||||
- Perfect for organizing large audio collections
|
|
||||||
|
|
||||||
### Automatic Discovery
|
|
||||||
- New files are found automatically on app restart
|
|
||||||
- Manual scan button for immediate discovery
|
|
||||||
- No configuration required
|
|
||||||
|
|
||||||
### File Management
|
|
||||||
- Files stay in organized directory structure
|
|
||||||
- Easy to add/remove files outside the app
|
|
||||||
- Clear organization by purpose/category
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### No Files Found
|
|
||||||
- **Check Directories**: Ensure audio files are in the correct subdirectories
|
|
||||||
- **File Formats**: Verify files are in supported formats (MP3, WAV, OGG, M4A, AAC, FLAC)
|
|
||||||
- **Permissions**: Make sure the app has read access to the audio directories
|
|
||||||
- **File Names**: Avoid special characters in file names
|
|
||||||
|
|
||||||
### Duplicate Files
|
|
||||||
- The scanner checks for existing files before adding
|
|
||||||
- Files already in your library won't be duplicated
|
|
||||||
- If you see duplicates, use the "🧹 Cleanup" button
|
|
||||||
|
|
||||||
### Web Mode Limitations
|
|
||||||
- Directory scanning only works in desktop mode (Electron)
|
|
||||||
- Web browsers can't access file system directories
|
|
||||||
- Use the Import buttons for manual file upload in web mode
|
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
### Organizing Your Audio
|
|
||||||
1. **Create Themed Folders**: Group similar audio files together
|
|
||||||
2. **Descriptive Names**: Use clear, descriptive file names
|
|
||||||
3. **Test Files**: Place a few test files first, then scan to verify
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- Scanning is fast for hundreds of files
|
|
||||||
- Large files (>100MB) may take longer to process
|
|
||||||
- The app shows progress messages during scanning
|
|
||||||
|
|
||||||
### Backup Strategy
|
|
||||||
- Keep original files in the audio directories
|
|
||||||
- The app creates references, not copies
|
|
||||||
- Easy to backup/restore entire audio collection
|
|
||||||
|
|
||||||
## Integration
|
|
||||||
|
|
||||||
### With Dynamic Audio System
|
|
||||||
- Scanned files integrate seamlessly with the dynamic audio system
|
|
||||||
- All audio management features work with scanned files
|
|
||||||
- Enable/disable scanned files individually
|
|
||||||
|
|
||||||
### With Existing Audio
|
|
||||||
- Scanned files are added to existing audio library
|
|
||||||
- No conflicts with manually imported files
|
|
||||||
- Use both methods together for maximum flexibility
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Organize Files**: Place your audio files in the appropriate directories
|
|
||||||
2. **Scan**: Use the "🔍 Scan Directories" button
|
|
||||||
3. **Test**: Try playing audio during a game session
|
|
||||||
4. **Manage**: Use the audio management UI to enable/disable specific files
|
|
||||||
21
ROADMAP.md
21
ROADMAP.md
|
|
@ -25,6 +25,12 @@
|
||||||
- Enhanced user experience improvements
|
- Enhanced user experience improvements
|
||||||
- Bug fixes and stability enhancements
|
- Bug fixes and stability enhancements
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
|
- **✅ 🎬 Porn Cinema Video Loading Bug Fix (October 31, 2025)**
|
||||||
|
- ✅ **Critical Bug Resolution**: Fixed video loading issue where Porn Cinema couldn't access videos from linked directories
|
||||||
|
- ✅ **Data Preservation Logic**: Implemented safeguards to prevent DesktopFileManager from overwriting existing video data
|
||||||
|
- ✅ **Storage System Enhancement**: Enhanced unified video library fallback mechanisms for reliable video access
|
||||||
|
- ✅ **Console Cleanup**: Removed excessive debug logging for cleaner user experience
|
||||||
|
- ✅ **Cross-Component Synchronization**: Fixed timing issues between main game and Porn Cinema video data access
|
||||||
- **🎬 Porn Cinema Media Player** *(✅ Major Progress - October 30-31, 2025)*
|
- **🎬 Porn Cinema Media Player** *(✅ Major Progress - October 30-31, 2025)*
|
||||||
- ✅ **Complete Layout Implementation**: Professional two-column design with main content area and right sidebar
|
- ✅ **Complete Layout Implementation**: Professional two-column design with main content area and right sidebar
|
||||||
- ✅ **Header Navigation**: Slim, modern header with Home, Settings, Theater, and Fullscreen controls
|
- ✅ **Header Navigation**: Slim, modern header with Home, Settings, Theater, and Fullscreen controls
|
||||||
|
|
@ -34,6 +40,7 @@
|
||||||
- ✅ **Auto-Hide Controls**: Smart control visibility with 3-second timeout during playback
|
- ✅ **Auto-Hide Controls**: Smart control visibility with 3-second timeout during playback
|
||||||
- ✅ **Video Library Integration**: Minimal, clean library section with grid/list views
|
- ✅ **Video Library Integration**: Minimal, clean library section with grid/list views
|
||||||
- ✅ **Responsive Design**: Clean mockup-matching layout with proper CSS architecture
|
- ✅ **Responsive Design**: Clean mockup-matching layout with proper CSS architecture
|
||||||
|
- ✅ **Video Loading System**: Robust video loading from unified video library with fallback mechanisms
|
||||||
- 🚧 **Next Steps**: Video library population, playlist functionality, search implementation
|
- 🚧 **Next Steps**: Video library population, playlist functionality, search implementation
|
||||||
- **🔧 Base Video Player Extraction** *(✅ COMPLETED - October 31, 2025)*
|
- **🔧 Base Video Player Extraction** *(✅ COMPLETED - October 31, 2025)*
|
||||||
- ✅ **Extract Reusable Components**: Created BaseVideoPlayer class with full video control functionality (400+ lines)
|
- ✅ **Extract Reusable Components**: Created BaseVideoPlayer class with full video control functionality (400+ lines)
|
||||||
|
|
@ -44,14 +51,22 @@
|
||||||
- ✅ **Script Integration**: Added baseVideoPlayer.js and focusVideoPlayer.js to index.html loading sequence
|
- ✅ **Script Integration**: Added baseVideoPlayer.js and focusVideoPlayer.js to index.html loading sequence
|
||||||
- ✅ **Global Export**: Properly exported classes to window object for browser compatibility
|
- ✅ **Global Export**: Properly exported classes to window object for browser compatibility
|
||||||
- ✅ **Syntax Validation**: Clean JavaScript validation with no errors
|
- ✅ **Syntax Validation**: Clean JavaScript validation with no errors
|
||||||
- **🎬 Porn Cinema Refactoring** *(✅ CORE COMPLETE - October 31, 2025)*
|
- **🎬 Porn Cinema Refactoring** *(✅ COMPLETED - October 31, 2025)*
|
||||||
- ✅ **Legacy Code Analysis**: Analyzed existing pornCinema.js for BaseVideoPlayer integration points
|
- ✅ **Legacy Code Analysis**: Analyzed existing pornCinema.js for BaseVideoPlayer integration points
|
||||||
- ✅ **Architecture Planning**: Identified cinema-specific features (playlist, theater mode, navigation)
|
- ✅ **Architecture Planning**: Identified cinema-specific features (playlist, theater mode, navigation)
|
||||||
- ✅ **Code Backup**: Created pornCinema-backup.js to preserve original implementation
|
- ✅ **Code Backup**: Created pornCinema-backup.js to preserve original implementation
|
||||||
- ✅ **Class Refactoring**: Created clean PornCinema class extending BaseVideoPlayer
|
- ✅ **Class Refactoring**: Created clean PornCinema class extending BaseVideoPlayer
|
||||||
- ✅ **Core Inheritance**: PornCinema now properly extends BaseVideoPlayer for shared functionality
|
- ✅ **Core Inheritance**: PornCinema now properly extends BaseVideoPlayer for shared functionality
|
||||||
- 📋 **Feature Migration**: Future enhancement to migrate advanced playlist and cinema UI features
|
- ✅ **Method Implementation**: Added initialize(), playVideo(), addToPlaylist() methods
|
||||||
- 📋 **Testing & Validation**: Comprehensive testing in cinema mode environment
|
- ✅ **Error Handling**: Proper TypeError resolution and method validation
|
||||||
|
- ✅ **Integration Testing**: Successfully tested in Option A architecture
|
||||||
|
- **🎮 Focus Interruption Video Integration** *(✅ COMPLETED - October 31, 2025)*
|
||||||
|
- ✅ **Video Library Access**: Fixed FocusVideoPlayer video library initialization
|
||||||
|
- ✅ **Volume Control Integration**: Added ultra-compact volume slider (60px width)
|
||||||
|
- ✅ **CSS Override System**: Resolved CSS conflicts with focus-volume-slider class
|
||||||
|
- ✅ **UI Refinement**: Achieved ultra-compact volume control design per user specs
|
||||||
|
- ✅ **Video Collection**: 34 videos successfully integrated from punishment/task/background categories
|
||||||
|
- ✅ **Testing Complete**: Focus interruption videos playing successfully with proper controls
|
||||||
- **NEW XP System Implementation:**
|
- **NEW XP System Implementation:**
|
||||||
- **Main Game**
|
- **Main Game**
|
||||||
- User gains 1 XP per task
|
- User gains 1 XP per task
|
||||||
|
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
/**
|
|
||||||
* Audio Compatibility Checker and Browser Mode Handler
|
|
||||||
* This script helps detect and handle audio limitations in browser vs Electron mode
|
|
||||||
*/
|
|
||||||
|
|
||||||
class AudioCompatibilityChecker {
|
|
||||||
constructor() {
|
|
||||||
this.isElectron = window.electronAPI !== undefined;
|
|
||||||
this.isFileProtocol = window.location.protocol === 'file:';
|
|
||||||
this.browserAudioSupported = this.checkBrowserAudioSupport();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkBrowserAudioSupport() {
|
|
||||||
// Test if we can create audio elements
|
|
||||||
try {
|
|
||||||
const testAudio = new Audio();
|
|
||||||
return testAudio !== null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Browser does not support Audio API:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testSingleAudioFile(path) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const audio = new Audio();
|
|
||||||
let resolved = false;
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
audio.pause();
|
|
||||||
audio.src = '';
|
|
||||||
audio.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
audio.addEventListener('canplay', () => {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve({ success: true, path, error: null });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.addEventListener('error', (e) => {
|
|
||||||
if (!resolved) {
|
|
||||||
const errorInfo = {
|
|
||||||
code: e.target.error?.code,
|
|
||||||
message: e.target.error?.message,
|
|
||||||
networkState: e.target.networkState,
|
|
||||||
readyState: e.target.readyState,
|
|
||||||
src: e.target.src
|
|
||||||
};
|
|
||||||
cleanup();
|
|
||||||
resolve({ success: false, path, error: errorInfo });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Timeout after 3 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve({ success: false, path, error: 'timeout' });
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// Try to load the audio
|
|
||||||
try {
|
|
||||||
audio.src = path;
|
|
||||||
audio.load();
|
|
||||||
} catch (error) {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve({ success: false, path, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async quickAudioTest() {
|
|
||||||
console.log('🎵 Audio Compatibility Test Starting...');
|
|
||||||
console.log(`🎵 Environment: ${this.isElectron ? 'Electron' : 'Browser'}`);
|
|
||||||
console.log(`🎵 Protocol: ${window.location.protocol}`);
|
|
||||||
console.log(`🎵 Audio API Support: ${this.browserAudioSupported}`);
|
|
||||||
|
|
||||||
// Test a few known audio files
|
|
||||||
const testFiles = [
|
|
||||||
'audio/tasks/teasing/u.mp3',
|
|
||||||
'audio/rewards/completion/u.mp3'
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
for (const file of testFiles) {
|
|
||||||
console.log(`🎵 Testing: ${file}`);
|
|
||||||
const result = await this.testSingleAudioFile(file);
|
|
||||||
results.push(result);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ ${file} - Working`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${file} - Failed:`, result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between tests
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecommendations() {
|
|
||||||
const recommendations = [];
|
|
||||||
|
|
||||||
if (!this.isElectron && this.isFileProtocol) {
|
|
||||||
recommendations.push({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Running in browser with file:// protocol',
|
|
||||||
suggestion: 'Use "npm start" to run the Electron desktop version for full audio support'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.browserAudioSupported) {
|
|
||||||
recommendations.push({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Browser does not support HTML5 Audio API',
|
|
||||||
suggestion: 'Try a different browser or update your current browser'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isElectron) {
|
|
||||||
recommendations.push({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Running in Electron desktop mode',
|
|
||||||
suggestion: 'Audio should work without restrictions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return recommendations;
|
|
||||||
}
|
|
||||||
|
|
||||||
async diagnoseAudioIssues() {
|
|
||||||
console.log('🎵 Audio Diagnostic Report');
|
|
||||||
console.log('=' .repeat(50));
|
|
||||||
|
|
||||||
const recommendations = this.getRecommendations();
|
|
||||||
recommendations.forEach(rec => {
|
|
||||||
const icon = rec.type === 'error' ? '❌' : rec.type === 'warning' ? '⚠️' : '✅';
|
|
||||||
console.log(`${icon} ${rec.message}`);
|
|
||||||
console.log(` 💡 ${rec.suggestion}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n🎵 Testing Audio Files...');
|
|
||||||
const testResults = await this.quickAudioTest();
|
|
||||||
|
|
||||||
const workingFiles = testResults.filter(r => r.success).length;
|
|
||||||
const totalFiles = testResults.length;
|
|
||||||
|
|
||||||
console.log('\n🎵 Summary:');
|
|
||||||
console.log(`✅ Working: ${workingFiles}/${totalFiles}`);
|
|
||||||
console.log(`❌ Failed: ${totalFiles - workingFiles}/${totalFiles}`);
|
|
||||||
|
|
||||||
if (workingFiles === 0) {
|
|
||||||
console.log('\n🎵 No audio files are working. This suggests:');
|
|
||||||
console.log(' - Browser CORS restrictions (use Electron app)');
|
|
||||||
console.log(' - Audio files are corrupted or missing');
|
|
||||||
console.log(' - Browser audio support issues');
|
|
||||||
} else if (workingFiles < totalFiles) {
|
|
||||||
console.log('\n🎵 Some audio files are working. Issues may be:');
|
|
||||||
console.log(' - Specific file corruption');
|
|
||||||
console.log(' - Filename/path issues');
|
|
||||||
console.log(' - Audio format compatibility');
|
|
||||||
} else {
|
|
||||||
console.log('\n🎵 All tested files are working! 🎉');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
environment: {
|
|
||||||
isElectron: this.isElectron,
|
|
||||||
isFileProtocol: this.isFileProtocol,
|
|
||||||
audioSupported: this.browserAudioSupported
|
|
||||||
},
|
|
||||||
testResults,
|
|
||||||
recommendations
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-run diagnostic if audio issues are detected
|
|
||||||
window.audioCompatibilityChecker = new AudioCompatibilityChecker();
|
|
||||||
|
|
||||||
// Quick check for common issues
|
|
||||||
if (!window.audioCompatibilityChecker.isElectron && window.audioCompatibilityChecker.isFileProtocol) {
|
|
||||||
console.log('🎵 Audio issues detected! Run window.audioCompatibilityChecker.diagnoseAudioIssues() for full report');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🎵 Audio Compatibility Checker loaded');
|
|
||||||
console.log('🎵 Run: window.audioCompatibilityChecker.diagnoseAudioIssues()');
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
/**
|
|
||||||
* Audio File Validator
|
|
||||||
* This script tests which audio files can actually be loaded by the browser
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function validateAudioFiles() {
|
|
||||||
console.log('🎵 Starting audio file validation...');
|
|
||||||
|
|
||||||
const audioStructure = {
|
|
||||||
tasks: {
|
|
||||||
teasing: [
|
|
||||||
'enjoying-a-giant-cock.mp3',
|
|
||||||
'horny-japanese-babe-japanese-sex.mp3',
|
|
||||||
'long-and-hard-moan.mp3',
|
|
||||||
'playing-her-pussy-with-a-dildo-japanese-sex.mp3',
|
|
||||||
'u.mp3'
|
|
||||||
],
|
|
||||||
intense: [
|
|
||||||
'bree-olson-screaming-orgasm-sound.mp3',
|
|
||||||
'carmela-bing-screaming-orgasm-sound.mp3',
|
|
||||||
'moaning-ringtone.mp3',
|
|
||||||
'multiple-orgasms-with-creamy-pussy.mp3'
|
|
||||||
],
|
|
||||||
instructions: [
|
|
||||||
'addict-to-my-tits-854x480p.mp3',
|
|
||||||
'deeper.mp3',
|
|
||||||
'you-love-to-goon.mp3'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
punishments: {
|
|
||||||
denial: [
|
|
||||||
'addicted-to-my-tits-tit-worship-mesmerize-joi-mov.mp3',
|
|
||||||
'dumb-gooner-bitch-4-big-tits.mp3',
|
|
||||||
'you-will-be-left-thoughtless.mp3'
|
|
||||||
],
|
|
||||||
mocking: [
|
|
||||||
'precise-denial.mp3',
|
|
||||||
'punishment-episode-cockring-causes-cumshots-cumshots-gameplay-only.mp3',
|
|
||||||
'quick-jerk-to-tits-in-pink-bra-1080p-ellie-idol.mp3',
|
|
||||||
'stroke-your-goon-bud.mp3'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
rewards: {
|
|
||||||
completion: ['u.mp3']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
working: [],
|
|
||||||
failed: [],
|
|
||||||
total: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [category, subcategories] of Object.entries(audioStructure)) {
|
|
||||||
for (const [subcategory, files] of Object.entries(subcategories)) {
|
|
||||||
for (const file of files) {
|
|
||||||
const audioPath = `audio/${category}/${subcategory}/${file}`;
|
|
||||||
results.total++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canLoad = await testAudioFile(audioPath);
|
|
||||||
if (canLoad) {
|
|
||||||
results.working.push(audioPath);
|
|
||||||
console.log(`✅ ${audioPath} - OK`);
|
|
||||||
} else {
|
|
||||||
results.failed.push(audioPath);
|
|
||||||
console.log(`❌ ${audioPath} - FAILED`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
results.failed.push(audioPath);
|
|
||||||
console.log(`❌ ${audioPath} - ERROR: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay to prevent browser overload
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎵 Audio Validation Results:');
|
|
||||||
console.log(`✅ Working files: ${results.working.length}/${results.total}`);
|
|
||||||
console.log(`❌ Failed files: ${results.failed.length}/${results.total}`);
|
|
||||||
|
|
||||||
if (results.failed.length > 0) {
|
|
||||||
console.log('\n❌ Failed files:');
|
|
||||||
results.failed.forEach(file => console.log(` - ${file}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.working.length > 0) {
|
|
||||||
console.log('\n✅ Working files:');
|
|
||||||
results.working.forEach(file => console.log(` - ${file}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testAudioFile(audioPath) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const audio = new Audio(audioPath);
|
|
||||||
let resolved = false;
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
audio.pause();
|
|
||||||
audio.src = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
audio.addEventListener('canplay', () => {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.addEventListener('error', () => {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Timeout after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!resolved) {
|
|
||||||
cleanup();
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
// Start loading
|
|
||||||
audio.load();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make functions available globally
|
|
||||||
window.audioValidator = {
|
|
||||||
validateAudioFiles,
|
|
||||||
testAudioFile
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('🎵 Audio validator loaded. Run: window.audioValidator.validateAudioFiles()');
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
/* Balanced Blue Theme - Saved Color Scheme */
|
|
||||||
:root {
|
|
||||||
/* Color system - Blue accents with neutral backgrounds */
|
|
||||||
--color-primary: #1e90ff;
|
|
||||||
--color-secondary: #0066cc;
|
|
||||||
--color-accent: #4da6ff;
|
|
||||||
--color-success: #28a745;
|
|
||||||
--color-warning: #ffc107;
|
|
||||||
--color-danger: #dc3545;
|
|
||||||
|
|
||||||
/* Text colors */
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #e0e0e0;
|
|
||||||
--text-tertiary: #b0b0b0;
|
|
||||||
--text-muted: #808080;
|
|
||||||
|
|
||||||
/* Background colors - Neutrals */
|
|
||||||
--bg-primary: #0a0a0a;
|
|
||||||
--bg-secondary: #1a1a1a;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--bg-card: rgba(42, 42, 42, 0.8);
|
|
||||||
--bg-modal: rgba(26, 26, 26, 0.95);
|
|
||||||
|
|
||||||
/* Border and shadow - Dark grays with blue accents */
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-accent: rgba(30, 144, 255, 0.3);
|
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
--glow-primary: 0 0 20px rgba(30, 144, 255, 0.4);
|
|
||||||
|
|
||||||
/* Font system */
|
|
||||||
--font-xs: 0.75rem;
|
|
||||||
--font-sm: 0.875rem;
|
|
||||||
--font-base: 1rem;
|
|
||||||
--font-lg: 1.125rem;
|
|
||||||
--font-xl: 1.25rem;
|
|
||||||
--font-xxl: 1.5rem;
|
|
||||||
--font-xxxl: 2rem;
|
|
||||||
--font-xxxxl: 3rem;
|
|
||||||
|
|
||||||
/* Spacing system */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-base: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-xxl: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Key styling notes for Balanced Blue Theme:
|
|
||||||
* - Backgrounds: Pure blacks and dark grays for excellent readability
|
|
||||||
* - Borders: Subtle white borders for clean separation
|
|
||||||
* - Blue accents: Professional tech blues - dodger blue, dark blue, light blue
|
|
||||||
* - Hover effects: Blue accent borders and subtle glows
|
|
||||||
* - Title: Clean blue gradient (dodger blue to dark blue) with animated glow
|
|
||||||
* - Progress bars: Professional blue gradient fills
|
|
||||||
* - Overall feel: Tech-sophisticated, clean, trustworthy, modern
|
|
||||||
*/
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
/* Balanced Forest Green Theme - Saved Color Scheme */
|
|
||||||
:root {
|
|
||||||
/* Color system - Forest green accents with neutral backgrounds */
|
|
||||||
--color-primary: #228b22;
|
|
||||||
--color-secondary: #006400;
|
|
||||||
--color-accent: #32cd32;
|
|
||||||
--color-success: #28a745;
|
|
||||||
--color-warning: #ffc107;
|
|
||||||
--color-danger: #dc3545;
|
|
||||||
|
|
||||||
/* Text colors */
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #e0e0e0;
|
|
||||||
--text-tertiary: #b0b0b0;
|
|
||||||
--text-muted: #808080;
|
|
||||||
|
|
||||||
/* Background colors - Neutrals */
|
|
||||||
--bg-primary: #0a0a0a;
|
|
||||||
--bg-secondary: #1a1a1a;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--bg-card: rgba(42, 42, 42, 0.8);
|
|
||||||
--bg-modal: rgba(26, 26, 26, 0.95);
|
|
||||||
|
|
||||||
/* Border and shadow - Dark grays with forest green accents */
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-accent: rgba(34, 139, 34, 0.3);
|
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
--glow-primary: 0 0 20px rgba(34, 139, 34, 0.4);
|
|
||||||
|
|
||||||
/* Font system */
|
|
||||||
--font-xs: 0.75rem;
|
|
||||||
--font-sm: 0.875rem;
|
|
||||||
--font-base: 1rem;
|
|
||||||
--font-lg: 1.125rem;
|
|
||||||
--font-xl: 1.25rem;
|
|
||||||
--font-xxl: 1.5rem;
|
|
||||||
--font-xxxl: 2rem;
|
|
||||||
--font-xxxxl: 3rem;
|
|
||||||
|
|
||||||
/* Spacing system */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-base: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-xxl: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Key styling notes for Balanced Forest Green Theme:
|
|
||||||
* - Backgrounds: Pure blacks and dark grays for excellent readability
|
|
||||||
* - Borders: Subtle white borders for clean separation
|
|
||||||
* - Forest green accents: Natural, earthy greens - forest green, dark green, lime green
|
|
||||||
* - Hover effects: Forest green accent borders and subtle glows
|
|
||||||
* - Title: Natural forest green gradient with animated glow
|
|
||||||
* - Progress bars: Organic green gradient fills
|
|
||||||
* - Overall feel: Natural, organic, earthy, calming forest environment
|
|
||||||
*/
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
/* Balanced Purple Theme - Saved Color Scheme */
|
|
||||||
:root {
|
|
||||||
/* Color system - Purple accents with neutral backgrounds */
|
|
||||||
--color-primary: #8a2be2;
|
|
||||||
--color-secondary: #da70d6;
|
|
||||||
--color-accent: #ba55d3;
|
|
||||||
--color-success: #28a745;
|
|
||||||
--color-warning: #ffc107;
|
|
||||||
--color-danger: #dc3545;
|
|
||||||
|
|
||||||
/* Text colors */
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #e0e0e0;
|
|
||||||
--text-tertiary: #b0b0b0;
|
|
||||||
--text-muted: #808080;
|
|
||||||
|
|
||||||
/* Background colors - Neutrals with subtle purple hints */
|
|
||||||
--bg-primary: #0a0a0a;
|
|
||||||
--bg-secondary: #1a1a1a;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--bg-card: rgba(42, 42, 42, 0.8);
|
|
||||||
--bg-modal: rgba(26, 26, 26, 0.95);
|
|
||||||
|
|
||||||
/* Border and shadow - Dark grays with purple accents */
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-accent: rgba(138, 43, 226, 0.3);
|
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
--glow-primary: 0 0 20px rgba(138, 43, 226, 0.4);
|
|
||||||
|
|
||||||
/* Font system */
|
|
||||||
--font-xs: 0.75rem;
|
|
||||||
--font-sm: 0.875rem;
|
|
||||||
--font-base: 1rem;
|
|
||||||
--font-lg: 1.125rem;
|
|
||||||
--font-xl: 1.25rem;
|
|
||||||
--font-xxl: 1.5rem;
|
|
||||||
--font-xxxl: 2rem;
|
|
||||||
--font-xxxxl: 3rem;
|
|
||||||
|
|
||||||
/* Spacing system */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-base: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-xxl: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Key styling notes for Balanced Purple Theme:
|
|
||||||
* - Backgrounds: Pure blacks and dark grays for excellent readability
|
|
||||||
* - Borders: Subtle white borders for clean separation
|
|
||||||
* - Purple accents: Used strategically for buttons, highlights, and interactive elements
|
|
||||||
* - Hover effects: Purple accent borders and subtle glows
|
|
||||||
* - Title: Purple gradient with animated glow
|
|
||||||
* - Progress bars: Purple gradient fills
|
|
||||||
* - Overall feel: Sophisticated, readable, with strategic purple branding
|
|
||||||
*/
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
/* Balanced Red/Crimson Theme - Saved Color Scheme (Cool Reds) */
|
|
||||||
:root {
|
|
||||||
/* Color system - Red accents with neutral backgrounds */
|
|
||||||
--color-primary: #dc143c;
|
|
||||||
--color-secondary: #b91c3c;
|
|
||||||
--color-accent: #e53e5a;
|
|
||||||
--color-success: #28a745;
|
|
||||||
--color-warning: #ffc107;
|
|
||||||
--color-danger: #dc3545;
|
|
||||||
|
|
||||||
/* Text colors */
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #e0e0e0;
|
|
||||||
--text-tertiary: #b0b0b0;
|
|
||||||
--text-muted: #808080;
|
|
||||||
|
|
||||||
/* Background colors - Neutrals */
|
|
||||||
--bg-primary: #0a0a0a;
|
|
||||||
--bg-secondary: #1a1a1a;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--bg-card: rgba(42, 42, 42, 0.8);
|
|
||||||
--bg-modal: rgba(26, 26, 26, 0.95);
|
|
||||||
|
|
||||||
/* Border and shadow - Dark grays with red accents */
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-accent: rgba(220, 20, 60, 0.3);
|
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
--glow-primary: 0 0 20px rgba(220, 20, 60, 0.4);
|
|
||||||
|
|
||||||
/* Font system */
|
|
||||||
--font-xs: 0.75rem;
|
|
||||||
--font-sm: 0.875rem;
|
|
||||||
--font-base: 1rem;
|
|
||||||
--font-lg: 1.125rem;
|
|
||||||
--font-xl: 1.25rem;
|
|
||||||
--font-xxl: 1.5rem;
|
|
||||||
--font-xxxl: 2rem;
|
|
||||||
--font-xxxxl: 3rem;
|
|
||||||
|
|
||||||
/* Spacing system */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-base: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-xxl: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Key styling notes for Balanced Red/Crimson Theme:
|
|
||||||
* - Backgrounds: Pure blacks and dark grays for excellent readability
|
|
||||||
* - Borders: Subtle white borders for clean separation
|
|
||||||
* - Red accents: Cool reds without orange tones - crimson, dark red, rose red
|
|
||||||
* - Hover effects: Red accent borders and subtle glows
|
|
||||||
* - Title: Cool red gradient (crimson to dark red) with animated glow
|
|
||||||
* - Progress bars: Cool red gradient fills
|
|
||||||
* - Overall feel: Bold, commanding, but balanced with neutral backgrounds
|
|
||||||
*/
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
// Audio Debug Script - Clear corrupted audio data and trigger fresh scan
|
|
||||||
|
|
||||||
console.log('🧹 Starting comprehensive audio cleanup...');
|
|
||||||
|
|
||||||
// Clear existing customAudio data completely and restart fresh
|
|
||||||
if (window.game && window.game.dataManager) {
|
|
||||||
console.log('🗑️ Clearing all existing audio data to start fresh...');
|
|
||||||
|
|
||||||
// Completely reset audio storage
|
|
||||||
window.game.dataManager.set('customAudio', { background: [], ambient: [] });
|
|
||||||
console.log('✅ Cleared all customAudio storage');
|
|
||||||
|
|
||||||
// Clear any cached audio library
|
|
||||||
if (window.game.audioManager) {
|
|
||||||
window.game.audioManager.audioLibrary = {
|
|
||||||
background: { general: [] }
|
|
||||||
};
|
|
||||||
console.log('✅ Cleared audio library cache');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now trigger a fresh directory scan if available
|
|
||||||
if (window.game.audioManager && window.game.audioManager.scanAudioDirectories) {
|
|
||||||
console.log('🔍 Triggering fresh directory scan...');
|
|
||||||
window.game.audioManager.scanAudioDirectories().then((scannedFiles) => {
|
|
||||||
console.log(`✅ Fresh scan completed - found ${scannedFiles.length} files`);
|
|
||||||
|
|
||||||
// Refresh the audio manager
|
|
||||||
return window.game.audioManager.refreshAudioLibrary();
|
|
||||||
}).then(() => {
|
|
||||||
console.log('✅ Audio library refreshed with clean data');
|
|
||||||
|
|
||||||
// Refresh the audio gallery
|
|
||||||
if (window.game.loadAudioGallery) {
|
|
||||||
window.game.loadAudioGallery();
|
|
||||||
console.log('✅ Audio gallery refreshed');
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('❌ Error during fresh scan:', error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ Directory scanning not available - try the "Scan Directories" button in Manage Audio');
|
|
||||||
|
|
||||||
// Just refresh what we have
|
|
||||||
if (window.game.audioManager) {
|
|
||||||
window.game.audioManager.refreshAudioLibrary().then(() => {
|
|
||||||
console.log('✅ Audio library refreshed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error('❌ Game or dataManager not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🎯 Cleanup script completed. Check the console for results and try playing audio.');
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// Clear Audio Storage Script - Reset all audio data to start fresh
|
|
||||||
|
|
||||||
console.log('🧹 Clearing all audio storage data...');
|
|
||||||
|
|
||||||
// Clear all audio-related storage
|
|
||||||
if (window.game && window.game.dataManager) {
|
|
||||||
console.log('🗑️ Clearing customAudio storage...');
|
|
||||||
window.game.dataManager.set('customAudio', { background: [], ambient: [] });
|
|
||||||
|
|
||||||
console.log('🗑️ Clearing audio library cache...');
|
|
||||||
if (window.game.audioManager) {
|
|
||||||
window.game.audioManager.audioLibrary = {
|
|
||||||
background: { general: [] }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔄 Refreshing audio library...');
|
|
||||||
if (window.game.audioManager) {
|
|
||||||
window.game.audioManager.refreshAudioLibrary().then(() => {
|
|
||||||
console.log('✅ Audio library refreshed');
|
|
||||||
|
|
||||||
// Refresh the UI
|
|
||||||
if (window.game.loadAudioGallery) {
|
|
||||||
window.game.loadAudioGallery();
|
|
||||||
console.log('✅ Audio gallery refreshed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ All audio storage cleared successfully');
|
|
||||||
} else {
|
|
||||||
console.error('❌ Game or dataManager not available');
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
/**
|
|
||||||
* Audio Overlap Debug Test Script
|
|
||||||
*
|
|
||||||
* This script helps test that background sounds stop properly when progressing through tasks.
|
|
||||||
*
|
|
||||||
* To use:
|
|
||||||
* 1. Start the game and begin playing
|
|
||||||
* 2. Open the browser console (F12)
|
|
||||||
* 3. Copy and paste this script into the console
|
|
||||||
* 4. Press Enter to run the test
|
|
||||||
*
|
|
||||||
* The test will simulate rapid task progression and monitor for audio overlaps.
|
|
||||||
*/
|
|
||||||
|
|
||||||
console.log('🎵 Starting Audio Overlap Test...');
|
|
||||||
|
|
||||||
// Function to test audio stopping
|
|
||||||
function testAudioStopping() {
|
|
||||||
if (!window.game || !window.game.audioManager) {
|
|
||||||
console.error('❌ Game or AudioManager not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioManager = window.game.audioManager;
|
|
||||||
|
|
||||||
console.log('🎵 Testing audio category stopping...');
|
|
||||||
|
|
||||||
// Test rapid audio switching
|
|
||||||
let testCount = 0;
|
|
||||||
const maxTests = 5;
|
|
||||||
|
|
||||||
const testInterval = setInterval(() => {
|
|
||||||
testCount++;
|
|
||||||
console.log(`🎵 Test ${testCount}/${maxTests}: Playing task audio...`);
|
|
||||||
|
|
||||||
// Stop all audio first
|
|
||||||
audioManager.stopAllImmediate();
|
|
||||||
|
|
||||||
// Wait a bit, then play new audio
|
|
||||||
setTimeout(() => {
|
|
||||||
audioManager.playTaskAudio('teasing', { fadeIn: 500, loop: true });
|
|
||||||
|
|
||||||
// Check how many audio elements are playing after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentlyPlaying = audioManager.getCurrentlyPlaying();
|
|
||||||
const playingCount = Object.keys(currentlyPlaying).length;
|
|
||||||
|
|
||||||
console.log(`🎵 Test ${testCount}: ${playingCount} audio tracks currently playing`);
|
|
||||||
console.log('🎵 Playing audio details:', currentlyPlaying);
|
|
||||||
|
|
||||||
if (playingCount > 1) {
|
|
||||||
console.warn(`⚠️ WARNING: ${playingCount} tracks playing simultaneously!`);
|
|
||||||
} else {
|
|
||||||
console.log('✅ Audio overlap test passed');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
if (testCount >= maxTests) {
|
|
||||||
clearInterval(testInterval);
|
|
||||||
console.log('🎵 Audio overlap test completed');
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
setTimeout(() => {
|
|
||||||
audioManager.stopAllImmediate();
|
|
||||||
console.log('🎵 Test cleanup completed');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to monitor audio during actual gameplay
|
|
||||||
function monitorGameplayAudio() {
|
|
||||||
if (!window.game || !window.game.audioManager) {
|
|
||||||
console.error('❌ Game or AudioManager not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🎵 Starting gameplay audio monitoring...');
|
|
||||||
console.log('🎵 Quickly complete or skip several tasks to test audio overlap prevention');
|
|
||||||
|
|
||||||
// Monitor audio every 500ms
|
|
||||||
const monitorInterval = setInterval(() => {
|
|
||||||
const currentlyPlaying = window.game.audioManager.getCurrentlyPlaying();
|
|
||||||
const playingCount = Object.keys(currentlyPlaying).length;
|
|
||||||
|
|
||||||
if (playingCount > 1) {
|
|
||||||
console.warn(`⚠️ AUDIO OVERLAP DETECTED: ${playingCount} tracks playing!`);
|
|
||||||
console.log('🎵 Overlapping audio details:', currentlyPlaying);
|
|
||||||
} else if (playingCount === 1) {
|
|
||||||
console.log(`✅ Single audio track playing (normal)`);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Stop monitoring after 30 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
clearInterval(monitorInterval);
|
|
||||||
console.log('🎵 Audio monitoring stopped');
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
return monitorInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to test rapid task progression simulation
|
|
||||||
function simulateRapidProgression() {
|
|
||||||
if (!window.game || !window.game.gameState || !window.game.gameState.isRunning) {
|
|
||||||
console.error('❌ Game not running - start a game first');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🎵 Simulating rapid task progression...');
|
|
||||||
|
|
||||||
let progressCount = 0;
|
|
||||||
const maxProgress = 3;
|
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
progressCount++;
|
|
||||||
console.log(`🎵 Rapid progression test ${progressCount}/${maxProgress}`);
|
|
||||||
|
|
||||||
// Simulate completing a task (this should stop audio and start new audio)
|
|
||||||
if (window.game.gameState.isRunning) {
|
|
||||||
window.game.completeTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCount >= maxProgress) {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
console.log('🎵 Rapid progression test completed');
|
|
||||||
}
|
|
||||||
}, 200); // Very fast progression to test overlap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the tests
|
|
||||||
console.log('🎵 Audio Debug Test Menu:');
|
|
||||||
console.log('🎵 1. Run testAudioStopping() - Tests basic audio stopping');
|
|
||||||
console.log('🎵 2. Run monitorGameplayAudio() - Monitors for overlaps during gameplay');
|
|
||||||
console.log('🎵 3. Run simulateRapidProgression() - Simulates rapid task progression');
|
|
||||||
console.log('🎵 Example: testAudioStopping()');
|
|
||||||
|
|
||||||
// Make functions available globally for manual testing
|
|
||||||
window.audioDebugTest = {
|
|
||||||
testAudioStopping,
|
|
||||||
monitorGameplayAudio,
|
|
||||||
simulateRapidProgression
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('🎵 Functions available as window.audioDebugTest.*');
|
|
||||||
console.log('🎵 Quick test: window.audioDebugTest.testAudioStopping()');
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// 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()');
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
/**
|
|
||||||
* Electron Audio Path Debug Script
|
|
||||||
* Run this in the console to debug audio path issues in Electron
|
|
||||||
*/
|
|
||||||
|
|
||||||
function debugElectronAudioPaths() {
|
|
||||||
console.log('🔧 Electron Audio Path Debug');
|
|
||||||
console.log('=' .repeat(50));
|
|
||||||
|
|
||||||
console.log('Environment Info:');
|
|
||||||
console.log('- Is Electron:', window.electronAPI !== undefined);
|
|
||||||
console.log('- Current URL:', window.location.href);
|
|
||||||
console.log('- Current Protocol:', window.location.protocol);
|
|
||||||
console.log('- Current Directory:', window.location.href.replace('/index.html', ''));
|
|
||||||
|
|
||||||
// Test basic audio creation
|
|
||||||
console.log('\n🎵 Testing Basic Audio Creation:');
|
|
||||||
try {
|
|
||||||
const testAudio = new Audio();
|
|
||||||
console.log('✅ Audio element created successfully');
|
|
||||||
console.log('- Initial src:', testAudio.src);
|
|
||||||
|
|
||||||
// Test setting a simple relative path
|
|
||||||
testAudio.src = 'audio/tasks/teasing/u.mp3';
|
|
||||||
console.log('- After setting relative path:', testAudio.src);
|
|
||||||
|
|
||||||
// Test setting an absolute file path
|
|
||||||
const absolutePath = window.location.href.replace('/index.html', '') + '/audio/tasks/teasing/u.mp3';
|
|
||||||
testAudio.src = absolutePath;
|
|
||||||
console.log('- After setting absolute path:', testAudio.src);
|
|
||||||
|
|
||||||
testAudio.src = '';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error creating audio element:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test actual file access
|
|
||||||
console.log('\n📁 Testing File Access:');
|
|
||||||
|
|
||||||
const testPaths = [
|
|
||||||
'audio/tasks/teasing/u.mp3',
|
|
||||||
'./audio/tasks/teasing/u.mp3',
|
|
||||||
window.location.href.replace('/index.html', '') + '/audio/tasks/teasing/u.mp3'
|
|
||||||
];
|
|
||||||
|
|
||||||
testPaths.forEach((path, index) => {
|
|
||||||
console.log(`\nTest ${index + 1}: ${path}`);
|
|
||||||
|
|
||||||
const audio = new Audio();
|
|
||||||
|
|
||||||
audio.addEventListener('loadstart', () => {
|
|
||||||
console.log(` ✅ Load started for: ${path}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.addEventListener('canplay', () => {
|
|
||||||
console.log(` ✅ Can play: ${path}`);
|
|
||||||
audio.src = ''; // Clean up
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.addEventListener('error', (e) => {
|
|
||||||
console.log(` ❌ Error loading: ${path}`);
|
|
||||||
console.log(` Error details:`, {
|
|
||||||
error: e.target.error,
|
|
||||||
src: e.target.src,
|
|
||||||
networkState: e.target.networkState,
|
|
||||||
readyState: e.target.readyState
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
audio.src = path;
|
|
||||||
setTimeout(() => {
|
|
||||||
if (audio.readyState === 0 && audio.networkState !== 2) {
|
|
||||||
console.log(` ⏰ Timeout for: ${path}`);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Exception setting src: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test if electronAPI is available and working
|
|
||||||
if (window.electronAPI) {
|
|
||||||
console.log('\n⚡ Testing Electron API:');
|
|
||||||
|
|
||||||
window.electronAPI.fileExists('audio/tasks/teasing/u.mp3').then(exists => {
|
|
||||||
console.log('- File exists (relative):', exists);
|
|
||||||
}).catch(err => {
|
|
||||||
console.log('- Error checking file existence:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electronAPI.getAppPath().then(appPath => {
|
|
||||||
console.log('- App path:', appPath);
|
|
||||||
}).catch(err => {
|
|
||||||
console.log('- Error getting app path:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-run the debug
|
|
||||||
console.log('🔧 Electron Audio Debug loaded. Running automatic test...');
|
|
||||||
debugElectronAudioPaths();
|
|
||||||
|
|
||||||
// Make function available globally
|
|
||||||
window.debugElectronAudioPaths = debugElectronAudioPaths;
|
|
||||||
610
index.html
610
index.html
|
|
@ -486,13 +486,28 @@
|
||||||
|
|
||||||
<!-- Upload Section -->
|
<!-- Upload Section -->
|
||||||
<div class="upload-section">
|
<div class="upload-section">
|
||||||
<h3>🎥 Import Video Files</h3>
|
<h3>🎥 Video Library Management</h3>
|
||||||
<div class="upload-controls">
|
<p style="color: #ccc; font-size: 12px; margin-bottom: 15px;">
|
||||||
<button id="import-background-video-btn" class="btn btn-primary">🌄 Background Videos</button>
|
Link external directories to build your video library. All videos are scanned recursively (including subdirectories).
|
||||||
<button id="import-task-video-btn" class="btn btn-success">🎯 Task Videos</button>
|
</p>
|
||||||
<button id="import-reward-video-btn" class="btn btn-info">🏆 Reward Videos</button>
|
|
||||||
<button id="import-punishment-video-btn" class="btn btn-warning">⚠️ Punishment Videos</button>
|
<!-- Directory Management Section -->
|
||||||
<input type="file" id="video-upload-input" accept="video/*" multiple style="display: none;">
|
<div class="directory-management-section" style="margin-bottom: 20px; padding: 15px; background: #2a2a3a; border-radius: 8px;">
|
||||||
|
<h4><EFBFBD> Linked Directories</h4>
|
||||||
|
<div class="directory-controls" style="margin-bottom: 15px;">
|
||||||
|
<button id="add-video-directory-btn" class="btn btn-primary">➕ Add Directory</button>
|
||||||
|
<button id="refresh-all-directories-btn" class="btn btn-secondary">🔄 Refresh All</button>
|
||||||
|
<button id="clear-all-directories-btn" class="btn btn-warning"><EFBFBD>️ Clear All</button>
|
||||||
|
</div>
|
||||||
|
<div id="linked-directories-list" style="max-height: 200px; overflow-y: auto;">
|
||||||
|
<!-- Linked directories will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Count Display -->
|
||||||
|
<div class="video-stats-section" style="padding: 10px; background: #1a1a2a; border-radius: 4px; text-align: center;">
|
||||||
|
<strong id="total-video-count">0 videos total</strong>
|
||||||
|
<span id="directory-count" style="color: #aaa; margin-left: 10px;">0 directories linked</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-info desktop-feature">
|
<div class="upload-info desktop-feature">
|
||||||
<span>💻 Desktop: Native file dialogs • Supports MP4, WebM, OGV, MOV formats</span>
|
<span>💻 Desktop: Native file dialogs • Supports MP4, WebM, OGV, MOV formats</span>
|
||||||
|
|
@ -511,39 +526,20 @@
|
||||||
<!-- Video Gallery Section -->
|
<!-- Video Gallery Section -->
|
||||||
<div class="gallery-section">
|
<div class="gallery-section">
|
||||||
<div class="gallery-header">
|
<div class="gallery-header">
|
||||||
<h3>📹 Current Video Library</h3>
|
<h3>📹 Video Library</h3>
|
||||||
<div class="gallery-controls">
|
<div class="gallery-controls">
|
||||||
<button id="select-all-video-btn" class="btn btn-small btn-outline">Select All</button>
|
<button id="select-all-video-btn" class="btn btn-small btn-outline">Select All</button>
|
||||||
<button id="deselect-all-video-btn" class="btn btn-small btn-outline">Deselect All</button>
|
<button id="deselect-all-video-btn" class="btn btn-small btn-outline">Deselect All</button>
|
||||||
<button id="delete-selected-video-btn" class="btn btn-danger btn-small">Delete Selected</button>
|
<button id="delete-selected-video-btn" class="btn btn-danger btn-small">Remove Selected</button>
|
||||||
<button id="refresh-videos-btn" class="btn btn-secondary btn-small">🔄 Refresh</button>
|
<button id="refresh-videos-btn" class="btn btn-secondary btn-small">🔄 Refresh</button>
|
||||||
<span class="video-count">Loading video files...</span>
|
<span class="video-count">Loading videos...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Video Tabs -->
|
<!-- Single unified video gallery (no categories) -->
|
||||||
<div class="video-tabs">
|
|
||||||
<button id="background-video-tab" class="tab-btn active">🌄 Background</button>
|
|
||||||
<button id="task-video-tab" class="tab-btn">🎯 Tasks</button>
|
|
||||||
<button id="reward-video-tab" class="tab-btn">🏆 Rewards</button>
|
|
||||||
<button id="punishment-video-tab" class="tab-btn">⚠️ Punishments</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video-galleries-container">
|
<div class="video-galleries-container">
|
||||||
<div id="background-video-gallery" class="video-gallery active">
|
<div id="unified-video-gallery" class="video-gallery active">
|
||||||
<!-- Background video files will be populated here -->
|
<!-- All videos from all linked directories will be shown here -->
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="task-video-gallery" class="video-gallery">
|
|
||||||
<!-- Task video files will be populated here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="reward-video-gallery" class="video-gallery">
|
|
||||||
<!-- Reward video files will be populated here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="punishment-video-gallery" class="video-gallery">
|
|
||||||
<!-- Punishment video files will be populated here -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1797,61 +1793,29 @@
|
||||||
const category = activeTab.id.replace('-video-tab', '');
|
const category = activeTab.id.replace('-video-tab', '');
|
||||||
loadVideoGalleryContent(category);
|
loadVideoGalleryContent(category);
|
||||||
}
|
}
|
||||||
} // Video tab buttons
|
}
|
||||||
const videoTabs = [
|
|
||||||
{ id: 'background-video-tab', gallery: 'background-video-gallery', category: 'background' },
|
|
||||||
{ id: 'task-video-tab', gallery: 'task-video-gallery', category: 'task' },
|
|
||||||
{ id: 'reward-video-tab', gallery: 'reward-video-gallery', category: 'reward' },
|
|
||||||
{ id: 'punishment-video-tab', gallery: 'punishment-video-gallery', category: 'punishment' }
|
|
||||||
];
|
|
||||||
|
|
||||||
videoTabs.forEach(tab => {
|
// Directory management buttons
|
||||||
const tabBtn = document.getElementById(tab.id);
|
const addDirectoryBtn = document.getElementById('add-video-directory-btn');
|
||||||
if (tabBtn) {
|
if (addDirectoryBtn) {
|
||||||
tabBtn.addEventListener('click', () => {
|
addDirectoryBtn.addEventListener('click', () => {
|
||||||
console.log(`Switching to ${tab.category} videos`);
|
handleAddVideoDirectory();
|
||||||
|
});
|
||||||
// Update active tab
|
}
|
||||||
document.querySelectorAll('.video-tabs .tab-btn').forEach(btn => {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
});
|
|
||||||
tabBtn.classList.add('active');
|
|
||||||
|
|
||||||
// Hide all galleries first
|
const refreshAllBtn = document.getElementById('refresh-all-directories-btn');
|
||||||
document.querySelectorAll('.video-gallery').forEach(gallery => {
|
if (refreshAllBtn) {
|
||||||
gallery.classList.remove('active');
|
refreshAllBtn.addEventListener('click', () => {
|
||||||
gallery.style.display = 'none';
|
handleRefreshAllDirectories();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Show the selected gallery
|
|
||||||
const targetGallery = document.getElementById(tab.gallery);
|
|
||||||
if (targetGallery) {
|
|
||||||
targetGallery.classList.add('active');
|
|
||||||
targetGallery.style.display = 'grid';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load videos for this category
|
const clearAllDirectoriesBtn = document.getElementById('clear-all-directories-btn');
|
||||||
loadVideoGalleryContent(tab.category);
|
if (clearAllDirectoriesBtn) {
|
||||||
});
|
clearAllDirectoriesBtn.addEventListener('click', () => {
|
||||||
}
|
handleClearAllDirectories();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Import video buttons
|
|
||||||
const importButtons = [
|
|
||||||
{ id: 'import-background-video-btn', category: 'background' },
|
|
||||||
{ id: 'import-task-video-btn', category: 'task' },
|
|
||||||
{ id: 'import-reward-video-btn', category: 'reward' },
|
|
||||||
{ id: 'import-punishment-video-btn', category: 'punishment' }
|
|
||||||
];
|
|
||||||
|
|
||||||
importButtons.forEach(button => {
|
|
||||||
const btn = document.getElementById(button.id);
|
|
||||||
if (btn) {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
handleVideoImport(button.category);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gallery control buttons
|
// Gallery control buttons
|
||||||
const selectAllBtn = document.getElementById('select-all-video-btn');
|
const selectAllBtn = document.getElementById('select-all-video-btn');
|
||||||
|
|
@ -1952,50 +1916,20 @@
|
||||||
// Settings handlers
|
// Settings handlers
|
||||||
setupVideoSettingsHandlers();
|
setupVideoSettingsHandlers();
|
||||||
|
|
||||||
// Initial load - ensure only background tab is active
|
// Initialize the unified video system
|
||||||
// Hide all galleries first
|
|
||||||
document.querySelectorAll('.video-gallery').forEach(gallery => {
|
|
||||||
gallery.classList.remove('active');
|
|
||||||
gallery.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show only background gallery
|
|
||||||
const backgroundGallery = document.getElementById('background-video-gallery');
|
|
||||||
if (backgroundGallery) {
|
|
||||||
backgroundGallery.classList.add('active');
|
|
||||||
backgroundGallery.style.display = 'grid';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan for new videos if in desktop mode
|
|
||||||
if (window.electronAPI && window.desktopFileManager) {
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
console.log('🔍 Scanning for new videos...');
|
console.log('🔍 Initializing unified video library...');
|
||||||
|
|
||||||
// Clear existing video storage to force fresh scan
|
// The file manager will automatically load linked directories
|
||||||
localStorage.removeItem('videoFiles');
|
// No need to scan old category directories anymore
|
||||||
|
setTimeout(() => {
|
||||||
Promise.all([
|
updateDirectoryList();
|
||||||
window.desktopFileManager.scanDirectoryForVideos('background'),
|
updateVideoStats();
|
||||||
window.desktopFileManager.scanDirectoryForVideos('tasks'),
|
loadUnifiedVideoGallery();
|
||||||
window.desktopFileManager.scanDirectoryForVideos('rewards'),
|
}, 500);
|
||||||
window.desktopFileManager.scanDirectoryForVideos('punishments')
|
|
||||||
]).then(([backgroundVideos, taskVideos, rewardVideos, punishmentVideos]) => {
|
|
||||||
const allVideos = [...backgroundVideos, ...taskVideos, ...rewardVideos, ...punishmentVideos];
|
|
||||||
if (allVideos.length > 0) {
|
|
||||||
window.desktopFileManager.updateVideoStorage(allVideos);
|
|
||||||
console.log(`📹 Found ${allVideos.length} videos in directories`);
|
|
||||||
}
|
|
||||||
// Reload all galleries after scanning
|
|
||||||
loadVideoGalleryContent('background');
|
|
||||||
loadVideoGalleryContent('task');
|
|
||||||
loadVideoGalleryContent('reward');
|
|
||||||
loadVideoGalleryContent('punishment');
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Error scanning video directories:', error);
|
|
||||||
// Still load the galleries even if scanning fails
|
|
||||||
loadVideoGalleryContent('background');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
loadVideoGalleryContent('background');
|
// Browser fallback - try to load from unified storage
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any existing invalid blob URLs from previous sessions
|
// Clean up any existing invalid blob URLs from previous sessions
|
||||||
|
|
@ -2029,6 +1963,392 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAddVideoDirectory() {
|
||||||
|
console.log('Adding new video directory...');
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
window.desktopFileManager.addVideoDirectory().then((result) => {
|
||||||
|
console.log('Directory addition result:', result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
console.log('Updating UI after successful directory addition...');
|
||||||
|
|
||||||
|
// Update UI components
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
|
||||||
|
console.log('Current video count:', window.desktopFileManager.getAllVideos().length);
|
||||||
|
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification(`✅ Added directory: ${result.directory.name} (${result.videoCount} videos)`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Directory addition returned null/failed');
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Directory addition failed:', error);
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification('❌ Failed to add directory', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification('⚠️ Directory management only available in desktop version', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefreshAllDirectories() {
|
||||||
|
console.log('Refreshing all directories...');
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
window.desktopFileManager.refreshAllDirectories().then(() => {
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Directory refresh failed:', error);
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification('❌ Failed to refresh directories', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearAllDirectories() {
|
||||||
|
if (!confirm('Are you sure you want to unlink all video directories? This will not delete your actual video files.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Clearing all directories...');
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
// Remove all directories
|
||||||
|
const directories = [...window.desktopFileManager.externalVideoDirectories];
|
||||||
|
|
||||||
|
Promise.all(directories.map(dir => window.desktopFileManager.removeVideoDirectory(dir.id)))
|
||||||
|
.then(() => {
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification('<27>️ All directories unlinked', 'success');
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to clear directories:', error);
|
||||||
|
if (window.game && window.game.showNotification) {
|
||||||
|
window.game.showNotification('❌ Failed to clear directories', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDirectoryList() {
|
||||||
|
const listContainer = document.getElementById('linked-directories-list');
|
||||||
|
if (!listContainer) return;
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
const directories = window.desktopFileManager.getDirectoriesInfo();
|
||||||
|
|
||||||
|
if (directories.length === 0) {
|
||||||
|
listContainer.innerHTML = '<p style="color: #666; text-align: center; padding: 20px;">No directories linked yet. Click "Add Directory" to get started.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listContainer.innerHTML = directories.map(dir => `
|
||||||
|
<div class="directory-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; margin-bottom: 5px; background: #333; border-radius: 4px;">
|
||||||
|
<div>
|
||||||
|
<strong>${dir.name}</strong>
|
||||||
|
<br><small style="color: #aaa;">${dir.path}</small>
|
||||||
|
<br><small style="color: #888;">${dir.videoCount} videos</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-small btn-outline" onclick="removeDirectory(${dir.id})" style="color: #ff6b6b;">Remove</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDirectory(directoryId) {
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
window.desktopFileManager.removeVideoDirectory(directoryId).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVideoStats() {
|
||||||
|
const totalCountEl = document.getElementById('total-video-count');
|
||||||
|
const directoryCountEl = document.getElementById('directory-count');
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
const allVideos = window.desktopFileManager.getAllVideos();
|
||||||
|
const directories = window.desktopFileManager.getDirectoriesInfo();
|
||||||
|
|
||||||
|
if (totalCountEl) totalCountEl.textContent = `${allVideos.length} videos total`;
|
||||||
|
if (directoryCountEl) directoryCountEl.textContent = `${directories.length} directories linked`;
|
||||||
|
} else {
|
||||||
|
if (totalCountEl) totalCountEl.textContent = '0 videos total';
|
||||||
|
if (directoryCountEl) directoryCountEl.textContent = '0 directories linked';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUnifiedVideoGallery() {
|
||||||
|
const gallery = document.getElementById('unified-video-gallery');
|
||||||
|
console.log('Gallery element found:', !!gallery);
|
||||||
|
if (!gallery) {
|
||||||
|
console.error('❌ unified-video-gallery element not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading status for videos
|
||||||
|
const videoCount = document.querySelector('.video-count');
|
||||||
|
if (videoCount) {
|
||||||
|
videoCount.textContent = 'Loading videos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update main loading progress if still loading
|
||||||
|
if (window.game && !window.game.isInitialized) {
|
||||||
|
window.game.updateLoadingProgress(85, 'Loading video library...');
|
||||||
|
}
|
||||||
|
|
||||||
|
let videos = [];
|
||||||
|
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
videos = window.desktopFileManager.getAllVideos();
|
||||||
|
console.log(`Loading unified video gallery: ${videos.length} videos found`);
|
||||||
|
console.log('Sample video:', videos[0]);
|
||||||
|
} else {
|
||||||
|
// Fallback: try to load from unified storage
|
||||||
|
const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
|
||||||
|
videos = unifiedData.allVideos || [];
|
||||||
|
console.log('Loaded from localStorage:', videos.length, 'videos');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update video count display
|
||||||
|
if (videoCount) {
|
||||||
|
videoCount.textContent = `${videos.length} videos found`;
|
||||||
|
console.log('Updated video count display');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update main loading progress if still loading
|
||||||
|
if (window.game && !window.game.isInitialized) {
|
||||||
|
window.game.updateLoadingProgress(95, 'Video library loaded...');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videos.length === 0) {
|
||||||
|
console.log('No videos found, showing empty message');
|
||||||
|
gallery.innerHTML = `
|
||||||
|
<div class="no-videos-message" style="text-align: center; padding: 40px; color: #666;">
|
||||||
|
<h4>📁 No videos found</h4>
|
||||||
|
<p>Link video directories above to build your library</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Proceeding to load gallery with', videos.length, 'videos');
|
||||||
|
|
||||||
|
// For large collections, use performance optimizations
|
||||||
|
if (videos.length > 100) {
|
||||||
|
console.log('Using large gallery loading');
|
||||||
|
loadLargeVideoGallery(gallery, videos);
|
||||||
|
} else {
|
||||||
|
console.log('Using standard gallery loading');
|
||||||
|
loadStandardVideoGallery(gallery, videos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStandardVideoGallery(gallery, videos) {
|
||||||
|
console.log('🎬 Loading standard gallery with', videos.length, 'videos');
|
||||||
|
|
||||||
|
// Standard loading for smaller collections
|
||||||
|
const videoHTML = videos.map((video, index) => createVideoItem(video, index)).join('');
|
||||||
|
|
||||||
|
gallery.innerHTML = videoHTML;
|
||||||
|
|
||||||
|
setupVideoItemHandlers(gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLargeVideoGallery(gallery, videos) {
|
||||||
|
console.log(`🚀 Loading large video gallery with ${videos.length} videos using performance optimizations`);
|
||||||
|
|
||||||
|
// Show loading message
|
||||||
|
gallery.innerHTML = `
|
||||||
|
<div class="loading-message" style="text-align: center; padding: 40px; color: #888;">
|
||||||
|
<h4>📼 Loading ${videos.length} videos...</h4>
|
||||||
|
<div class="progress-bar" style="width: 100%; height: 4px; background: #333; border-radius: 2px; margin: 10px 0;">
|
||||||
|
<div id="video-load-progress" style="width: 0%; height: 100%; background: #673ab7; border-radius: 2px; transition: width 0.3s;"></div>
|
||||||
|
</div>
|
||||||
|
<p>Processing in batches for optimal performance</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Process videos in chunks to prevent UI blocking
|
||||||
|
const chunkSize = 50; // Process 50 videos at a time
|
||||||
|
let currentChunk = 0;
|
||||||
|
const totalChunks = Math.ceil(videos.length / chunkSize);
|
||||||
|
|
||||||
|
function processNextChunk() {
|
||||||
|
const startIndex = currentChunk * chunkSize;
|
||||||
|
const endIndex = Math.min(startIndex + chunkSize, videos.length);
|
||||||
|
const chunk = videos.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
const progress = ((currentChunk + 1) / totalChunks) * 100;
|
||||||
|
const progressBar = document.getElementById('video-load-progress');
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = `${progress}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChunk === 0) {
|
||||||
|
// First chunk - replace loading message with video grid
|
||||||
|
gallery.innerHTML = '<div class="video-grid-container"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = gallery.querySelector('.video-grid-container');
|
||||||
|
if (container) {
|
||||||
|
// Add this chunk's videos
|
||||||
|
const chunkHTML = chunk.map((video, localIndex) =>
|
||||||
|
createVideoItemLazy(video, startIndex + localIndex)
|
||||||
|
).join('');
|
||||||
|
container.insertAdjacentHTML('beforeend', chunkHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChunk++;
|
||||||
|
|
||||||
|
if (currentChunk < totalChunks) {
|
||||||
|
// Process next chunk with a small delay to keep UI responsive
|
||||||
|
setTimeout(processNextChunk, 10);
|
||||||
|
} else {
|
||||||
|
// All chunks processed
|
||||||
|
console.log(`✅ Gallery loading complete: ${videos.length} videos rendered`);
|
||||||
|
setupVideoItemHandlers(gallery);
|
||||||
|
setupLazyThumbnailLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing
|
||||||
|
setTimeout(processNextChunk, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoItem(video, index) {
|
||||||
|
return `
|
||||||
|
<div class="video-item" data-video-index="${index}" data-video-name="${video.name}" data-video-url="${video.url}">
|
||||||
|
<div class="video-thumbnail">
|
||||||
|
<video preload="metadata" muted style="width: 100%; height: 100%; object-fit: cover;"
|
||||||
|
onloadedmetadata="this.currentTime = 2">
|
||||||
|
<source src="${video.url}" type="${video.type || 'video/mp4'}">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="video-info">
|
||||||
|
<input type="checkbox" class="video-checkbox" data-video-index="${index}">
|
||||||
|
<div class="video-details">
|
||||||
|
<div class="video-title" title="${video.title}">${video.title}</div>
|
||||||
|
<div class="video-meta">
|
||||||
|
<small style="color: #888;">📁 ${video.directoryName || 'Unknown'}</small>
|
||||||
|
${video.size ? `<small style="color: #888;"> • ${formatFileSize(video.size)}</small>` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-small" onclick="previewVideo('${video.url}', '${video.title}')" style="margin-top: 8px;">▶️ Preview</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoItemLazy(video, index) {
|
||||||
|
return `
|
||||||
|
<div class="video-item" data-video-index="${index}" data-video-name="${video.name}" data-video-url="${video.url}">
|
||||||
|
<div class="video-thumbnail lazy-thumbnail" data-video-url="${video.url}" data-video-type="${video.type || 'video/mp4'}">
|
||||||
|
<div class="thumbnail-placeholder" style="width: 100%; height: 100%; background: #333; display: flex; align-items: center; justify-content: center; color: #666;">
|
||||||
|
<span>🎬</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-info">
|
||||||
|
<input type="checkbox" class="video-checkbox" data-video-index="${index}">
|
||||||
|
<div class="video-details">
|
||||||
|
<div class="video-title" title="${video.title}">${video.title}</div>
|
||||||
|
<div class="video-meta">
|
||||||
|
<small style="color: #888;">📁 ${video.directoryName || 'Unknown'}</small>
|
||||||
|
${video.size ? `<small style="color: #888;"> • ${formatFileSize(video.size)}</small>` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-small" onclick="previewVideo('${video.url}', '${video.title}')" style="margin-top: 8px;background: linear-gradient(135deg, #4a90e2, #357abd) !important;color: #ffffff !important;border: 1px solidabd !important;">▶️ Preview</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLazyThumbnailLoading() {
|
||||||
|
const lazyThumbnails = document.querySelectorAll('.lazy-thumbnail');
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const thumbnail = entry.target;
|
||||||
|
const videoUrl = thumbnail.dataset.videoUrl;
|
||||||
|
const videoType = thumbnail.dataset.videoType;
|
||||||
|
|
||||||
|
// Replace placeholder with actual video thumbnail
|
||||||
|
thumbnail.innerHTML = `
|
||||||
|
<video preload="metadata" muted style="width: 100%; height: 100%; object-fit: cover;" loading="lazy"
|
||||||
|
onloadedmetadata="this.currentTime = 2">
|
||||||
|
<source src="${videoUrl}" type="${videoType}">
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
|
||||||
|
thumbnail.classList.remove('lazy-thumbnail');
|
||||||
|
observer.unobserve(thumbnail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: '100px' // Start loading thumbnails 100px before they come into view
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyThumbnails.forEach(thumbnail => {
|
||||||
|
observer.observe(thumbnail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVideoItemHandlers(gallery) {
|
||||||
|
// Add click handlers for video selection
|
||||||
|
gallery.querySelectorAll('.video-item').forEach(item => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
if (e.target.type !== 'checkbox' && !e.target.closest('button')) {
|
||||||
|
const checkbox = item.querySelector('.video-checkbox');
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
item.classList.toggle('selected', checkbox.checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global function for removing directories (called from HTML onclick)
|
||||||
|
window.removeDirectory = function(directoryId) {
|
||||||
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
|
window.desktopFileManager.removeVideoDirectory(directoryId).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
function handleVideoImport(category) {
|
function handleVideoImport(category) {
|
||||||
console.log(`Importing ${category} videos...`);
|
console.log(`Importing ${category} videos...`);
|
||||||
|
|
||||||
|
|
@ -2163,8 +2483,10 @@
|
||||||
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
||||||
const dirCategory = category === 'task' ? 'tasks' :
|
const dirCategory = category === 'task' ? 'tasks' :
|
||||||
category === 'reward' ? 'rewards' :
|
category === 'reward' ? 'rewards' :
|
||||||
category === 'punishment' ? 'punishments' : category;
|
category === 'punishment' ? 'punishments' :
|
||||||
|
category; // Keep cinema, background as-is
|
||||||
videos = storedVideos[dirCategory] || [];
|
videos = storedVideos[dirCategory] || [];
|
||||||
|
console.log(`Loading ${category} videos:`, videos.length, 'videos found');
|
||||||
} else {
|
} else {
|
||||||
// Fallback to localStorage
|
// Fallback to localStorage
|
||||||
videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
||||||
|
|
@ -2468,33 +2790,20 @@
|
||||||
|
|
||||||
function refreshVideoLibrary() {
|
function refreshVideoLibrary() {
|
||||||
if (window.electronAPI && window.desktopFileManager) {
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
// Clear existing video storage
|
|
||||||
localStorage.removeItem('videoFiles');
|
|
||||||
|
|
||||||
if (window.game && window.game.showNotification) {
|
if (window.game && window.game.showNotification) {
|
||||||
window.game.showNotification('🔄 Refreshing video library...', 'info');
|
window.game.showNotification('🔄 Refreshing video library...', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rescan all directories
|
// Use the new unified system - refresh all linked directories
|
||||||
Promise.all([
|
window.desktopFileManager.refreshAllDirectories().then(() => {
|
||||||
window.desktopFileManager.scanDirectoryForVideos('background'),
|
// Update UI
|
||||||
window.desktopFileManager.scanDirectoryForVideos('tasks'),
|
updateDirectoryList();
|
||||||
window.desktopFileManager.scanDirectoryForVideos('rewards'),
|
updateVideoStats();
|
||||||
window.desktopFileManager.scanDirectoryForVideos('punishments')
|
loadUnifiedVideoGallery();
|
||||||
]).then(([backgroundVideos, taskVideos, rewardVideos, punishmentVideos]) => {
|
|
||||||
const allVideos = [...backgroundVideos, ...taskVideos, ...rewardVideos, ...punishmentVideos];
|
|
||||||
if (allVideos.length > 0) {
|
|
||||||
window.desktopFileManager.updateVideoStorage(allVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload all galleries
|
|
||||||
loadVideoGalleryContent('background');
|
|
||||||
loadVideoGalleryContent('task');
|
|
||||||
loadVideoGalleryContent('reward');
|
|
||||||
loadVideoGalleryContent('punishment');
|
|
||||||
|
|
||||||
if (window.game && window.game.showNotification) {
|
if (window.game && window.game.showNotification) {
|
||||||
window.game.showNotification(`✅ Video library refreshed! Found ${allVideos.length} videos`, 'success');
|
const videoCount = window.desktopFileManager.getAllVideos().length;
|
||||||
|
window.game.showNotification(`✅ Video library refreshed! Found ${videoCount} videos`, 'success');
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error refreshing video library:', error);
|
console.error('Error refreshing video library:', error);
|
||||||
|
|
@ -2611,6 +2920,13 @@
|
||||||
if (window.electronAPI && typeof DesktopFileManager !== 'undefined') {
|
if (window.electronAPI && typeof DesktopFileManager !== 'undefined') {
|
||||||
window.desktopFileManager = new DesktopFileManager(window.game?.dataManager);
|
window.desktopFileManager = new DesktopFileManager(window.game?.dataManager);
|
||||||
console.log('🖥️ Desktop File Manager initialized for video management');
|
console.log('🖥️ Desktop File Manager initialized for video management');
|
||||||
|
|
||||||
|
// Initialize the new unified video system
|
||||||
|
setTimeout(() => {
|
||||||
|
updateDirectoryList();
|
||||||
|
updateVideoStats();
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up video management button (only once)
|
// Set up video management button (only once)
|
||||||
|
|
|
||||||
|
|
@ -248,9 +248,40 @@
|
||||||
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
|
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
|
||||||
console.log('🖥️ Desktop File Manager initialized for porn cinema');
|
console.log('🖥️ Desktop File Manager initialized for porn cinema');
|
||||||
|
|
||||||
// Wait a moment for the desktop file manager to fully initialize
|
// Wait for the desktop file manager to fully initialize
|
||||||
// The init() method is async and sets up video directories
|
// This includes loading linked directories and video files
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
let retries = 0;
|
||||||
|
const maxRetries = 50; // Wait up to 5 seconds
|
||||||
|
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
// Check if initialization is complete by verifying video directories are set up
|
||||||
|
if (window.desktopFileManager.videoDirectories &&
|
||||||
|
window.desktopFileManager.videoDirectories.background) {
|
||||||
|
console.log('✅ Desktop file manager video directories are ready');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⏳ Waiting for desktop file manager to initialize... (${retries + 1}/${maxRetries})`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries >= maxRetries) {
|
||||||
|
console.warn('⚠️ Desktop file manager took too long to initialize');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force refresh of linked directories to ensure we have the latest video data
|
||||||
|
try {
|
||||||
|
// Force reload from storage first
|
||||||
|
await window.desktopFileManager.loadLinkedDirectories();
|
||||||
|
console.log('✅ Force reloaded linked directories');
|
||||||
|
|
||||||
|
// Then refresh all directories to get current video lists
|
||||||
|
await window.desktopFileManager.refreshAllDirectories();
|
||||||
|
console.log('✅ Refreshed all video directories');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Error refreshing directories:', error);
|
||||||
|
}
|
||||||
|
|
||||||
} else if (!window.electronAPI) {
|
} else if (!window.electronAPI) {
|
||||||
console.warn('⚠️ Running in browser mode - video management limited');
|
console.warn('⚠️ Running in browser mode - video management limited');
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// Remove effects and conditions objects from game.js
|
|
||||||
const filePath = 'src/core/game.js';
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Remove effects objects
|
|
||||||
content = content.replace(/,?\s*effects:\s*{[^}]+}/g, '');
|
|
||||||
|
|
||||||
// Remove conditions objects
|
|
||||||
content = content.replace(/,?\s*conditions:\s*{[^}]+}/g, '');
|
|
||||||
|
|
||||||
// Clean up orphaned commas
|
|
||||||
content = content.replace(/\{\s*,/g, '{');
|
|
||||||
content = content.replace(/,\s*\}/g, '}');
|
|
||||||
content = content.replace(/,(\s*),/g, ',');
|
|
||||||
|
|
||||||
// Clean up any lines that only have commas
|
|
||||||
content = content.split('\n').map(line => {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (trimmed === ',' || trimmed === ',,' || trimmed === ',,,') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content, 'utf8');
|
|
||||||
console.log('Effects and conditions objects removed from game.js');
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// Remove template variables from game.js story text
|
|
||||||
const filePath = 'src/core/game.js';
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Replace template variables with generic text
|
|
||||||
content = content.replace(/Your \{arousal\} level is showing, and your \{control\} needs work/g, 'Your level is showing, and you need to focus');
|
|
||||||
content = content.replace(/Your final stats show \{arousal\} arousal and \{control\} control/g, 'You have completed the session');
|
|
||||||
content = content.replace(/Your final arousal level of \{arousal\} and control level of \{control\}/g, 'Your performance');
|
|
||||||
content = content.replace(/Your arousal is at \{arousal\} and your control is \{control\}/g, 'The punishment is having its effect');
|
|
||||||
content = content.replace(/Your final arousal of \{arousal\} and broken control of \{control\}/g, 'Your state');
|
|
||||||
content = content.replace(/Arousal: \{arousal\}, Control: \{control\}/g, 'Your session state');
|
|
||||||
content = content.replace(/Final state - Arousal: \{arousal\}, Control: \{control\}/g, 'Final state recorded');
|
|
||||||
content = content.replace(/Your \{arousal\} is showing/g, 'Your state is evident');
|
|
||||||
content = content.replace(/Your arousal at \{arousal\} and diminished control at \{control\}/g, 'Your state and responses');
|
|
||||||
content = content.replace(/Final arousal: \{arousal\}, Control: \{control\}/g, 'Final state recorded');
|
|
||||||
|
|
||||||
// Remove any remaining isolated template variables
|
|
||||||
content = content.replace(/\{arousal\}/g, 'your state');
|
|
||||||
content = content.replace(/\{control\}/g, 'your focus');
|
|
||||||
content = content.replace(/\{intensity\}/g, 'the level');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content, 'utf8');
|
|
||||||
console.log('Template variables removed from game.js story text');
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// Remove all remaining counter code from interactiveTaskManager.js
|
|
||||||
const filePath = 'src/features/tasks/interactiveTaskManager.js';
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Remove the default state initialization
|
|
||||||
content = content.replace(/arousal: 50,.*?\/\/ 0-100 scale\s*\n/g, '');
|
|
||||||
content = content.replace(/control: 50,.*?\/\/ 0-100 scale.*?\n/g, '');
|
|
||||||
content = content.replace(/intensity: 1,.*?\/\/ 1-3 scale\s*\n/g, '');
|
|
||||||
|
|
||||||
// Remove the effect application functions
|
|
||||||
content = content.replace(/applyChoiceEffects\(choice, state\)\s*\{[\s\S]*?\n\s*\}/g, 'applyChoiceEffects(choice, state) {\n // Effects system removed\n }');
|
|
||||||
content = content.replace(/applyActionEffects\(step, state\)\s*\{[\s\S]*?\n\s*\}/g, 'applyActionEffects(step, state) {\n // Effects system removed\n }');
|
|
||||||
|
|
||||||
// Remove any remaining counter processing in photo selection logic
|
|
||||||
content = content.replace(/const arousal = state\.arousal \|\| 50;/g, '// Counter system removed');
|
|
||||||
content = content.replace(/const control = state\.control \|\| 50;/g, '// Counter system removed');
|
|
||||||
|
|
||||||
// Replace arousal-based photo logic with simple static logic
|
|
||||||
content = content.replace(/if \(arousal >= 80\) \{[\s\S]*?\} else if \(arousal >= 60\) \{[\s\S]*?\} else if \(arousal >= 40\) \{[\s\S]*?\}/g, 'photoCount += 1; // Static photo count');
|
|
||||||
|
|
||||||
// Remove any conditional logic based on control
|
|
||||||
content = content.replace(/if \(control >= 70\) \{[\s\S]*?\}/g, '// Control-based logic removed');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content, 'utf8');
|
|
||||||
console.log('Counter code removed from interactiveTaskManager.js');
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
/**
|
|
||||||
* Script to remove orphaned commas and fix JSON structure in game mode files
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const gameModeFiles = [
|
|
||||||
'src/data/modes/trainingGameData.js',
|
|
||||||
'src/data/modes/humiliationGameData.js',
|
|
||||||
'src/data/modes/dressUpGameData.js',
|
|
||||||
'src/data/modes/enduranceGameData.js'
|
|
||||||
];
|
|
||||||
|
|
||||||
function cleanOrphanedCommas(filePath) {
|
|
||||||
console.log(`Cleaning orphaned commas in: ${filePath}`);
|
|
||||||
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Remove lines that are just commas with whitespace
|
|
||||||
content = content.replace(/^\s*,\s*$/gm, '');
|
|
||||||
|
|
||||||
// Remove double commas
|
|
||||||
content = content.replace(/,,+/g, ',');
|
|
||||||
|
|
||||||
// Fix lines where comma is separated from the property
|
|
||||||
// Find patterns like: property: "value"\n,\n and fix to property: "value",\n
|
|
||||||
content = content.replace(/(["\w]+:\s*"[^"]*")\s*\n\s*,\s*\n/g, '$1,\n');
|
|
||||||
content = content.replace(/(["\w]+:\s*\d+)\s*\n\s*,\s*\n/g, '$1,\n');
|
|
||||||
content = content.replace(/(["\w]+:\s*'[^']*')\s*\n\s*,\s*\n/g, '$1,\n');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content);
|
|
||||||
console.log(`Cleaned orphaned commas in: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
console.log('Cleaning orphaned commas in game mode files...');
|
|
||||||
|
|
||||||
gameModeFiles.forEach(file => {
|
|
||||||
const fullPath = path.join(process.cwd(), file);
|
|
||||||
if (fs.existsSync(fullPath)) {
|
|
||||||
cleanOrphanedCommas(fullPath);
|
|
||||||
} else {
|
|
||||||
console.log(`File not found: ${fullPath}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('All orphaned commas cleaned!');
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* Script to fix missing commas in game mode data files after effects removal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const gameModeFiles = [
|
|
||||||
'src/data/modes/trainingGameData.js',
|
|
||||||
'src/data/modes/humiliationGameData.js',
|
|
||||||
'src/data/modes/dressUpGameData.js',
|
|
||||||
'src/data/modes/enduranceGameData.js'
|
|
||||||
];
|
|
||||||
|
|
||||||
function fixCommasInFile(filePath) {
|
|
||||||
console.log(`Fixing commas in: ${filePath}`);
|
|
||||||
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Fix missing commas after preview/type/text lines followed by nextStep with proper spacing
|
|
||||||
content = content.replace(/("preview": "[^"]*")\s*\n(\s*nextStep:)/g, '$1,\n$2');
|
|
||||||
content = content.replace(/("type": "[^"]*")\s*\n(\s*nextStep:)/g, '$1,\n$2');
|
|
||||||
content = content.replace(/("text": "[^"]*")\s*\n(\s*nextStep:)/g, '$1,\n$2');
|
|
||||||
|
|
||||||
// Fix missing commas after any property followed by nextStep (without quotes around nextStep)
|
|
||||||
content = content.replace(/([^,])\s*\n(\s*nextStep:\s*"[^"]*")/g, '$1,\n$2');
|
|
||||||
|
|
||||||
// Fix missing commas in choice/step objects - between } and { on different lines
|
|
||||||
content = content.replace(/(\})\s*\n(\s*\{)/g, '$1,\n$2');
|
|
||||||
|
|
||||||
// Fix missing commas after duration/actionText followed by nextStep
|
|
||||||
content = content.replace(/(duration: \d+)\s*\n(\s*nextStep:)/g, '$1,\n$2');
|
|
||||||
content = content.replace(/("actionText": "[^"]*")\s*\n(\s*nextStep:)/g, '$1,\n$2');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content);
|
|
||||||
console.log(`Fixed commas in: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
console.log('Fixing missing commas in game mode files...');
|
|
||||||
|
|
||||||
gameModeFiles.forEach(file => {
|
|
||||||
const fullPath = path.join(process.cwd(), file);
|
|
||||||
if (fs.existsSync(fullPath)) {
|
|
||||||
fixCommasInFile(fullPath);
|
|
||||||
} else {
|
|
||||||
console.log(`File not found: ${fullPath}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('All comma issues fixed!');
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Read the current gameModeManager.js
|
|
||||||
const filePath = path.join(__dirname, '..', 'src', 'core', 'gameModeManager.js');
|
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Find the start of getScenarioData method and the start of getScenarioFromGame method
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
let startRemoval = -1;
|
|
||||||
let endRemoval = -1;
|
|
||||||
|
|
||||||
// Find where the problematic data starts
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].trim() === 'return this.getScenarioFromGame(scenarioId);' && startRemoval === -1) {
|
|
||||||
startRemoval = i + 1; // Start removing after this line
|
|
||||||
}
|
|
||||||
if (lines[i].trim().includes('if (window.gameData && window.gameData.mainTasks)')) {
|
|
||||||
endRemoval = i - 1; // Stop removing before this line
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found removal range: lines ${startRemoval + 1} to ${endRemoval + 1}`);
|
|
||||||
|
|
||||||
if (startRemoval !== -1 && endRemoval !== -1 && startRemoval < endRemoval) {
|
|
||||||
// Remove the problematic lines
|
|
||||||
const cleanedLines = [
|
|
||||||
...lines.slice(0, startRemoval),
|
|
||||||
' }',
|
|
||||||
'',
|
|
||||||
' /**',
|
|
||||||
' * Get scenario data from the current game.js implementation',
|
|
||||||
' */',
|
|
||||||
' getScenarioFromGame(scenarioId) {',
|
|
||||||
' // This accesses the scenarios currently defined in game.js',
|
|
||||||
...lines.slice(endRemoval + 1)
|
|
||||||
];
|
|
||||||
|
|
||||||
const cleanedContent = cleanedLines.join('\n');
|
|
||||||
fs.writeFileSync(filePath, cleanedContent, 'utf8');
|
|
||||||
console.log('Fixed gameModeManager.js by removing orphaned scenario data');
|
|
||||||
console.log(`Removed ${endRemoval - startRemoval + 1} lines`);
|
|
||||||
} else {
|
|
||||||
console.log('Could not find proper removal boundaries');
|
|
||||||
console.log(`Start: ${startRemoval}, End: ${endRemoval}`);
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* Script to remove all arousal/control/intensity effects from game mode data files
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const gameModeFiles = [
|
|
||||||
'src/data/modes/trainingGameData.js',
|
|
||||||
'src/data/modes/humiliationGameData.js',
|
|
||||||
'src/data/modes/dressUpGameData.js',
|
|
||||||
'src/data/modes/enduranceGameData.js'
|
|
||||||
];
|
|
||||||
|
|
||||||
function removeEffectsFromFile(filePath) {
|
|
||||||
console.log(`Processing: ${filePath}`);
|
|
||||||
|
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Remove effects objects with various formats
|
|
||||||
// Match: effects: { ... },
|
|
||||||
content = content.replace(/\s*effects: \{[^}]*\},?\s*/g, '');
|
|
||||||
|
|
||||||
// Remove effects objects without trailing comma
|
|
||||||
content = content.replace(/\s*effects: \{[^}]*\}\s*/g, '');
|
|
||||||
|
|
||||||
// Clean up any orphaned commas and spacing issues
|
|
||||||
content = content.replace(/,(\s*nextStep:)/g, '\n $1');
|
|
||||||
content = content.replace(/,(\s*\})/g, '$1');
|
|
||||||
|
|
||||||
// Remove effects from ending text interpolation
|
|
||||||
content = content.replace(/\{arousal\}/g, 'HIGH');
|
|
||||||
content = content.replace(/\{control\}/g, 'VARIABLE');
|
|
||||||
content = content.replace(/\{intensity\}/g, 'MAXIMUM');
|
|
||||||
|
|
||||||
// Clean up any references to arousal/control/intensity in story text
|
|
||||||
content = content.replace(/building arousal/g, 'building excitement');
|
|
||||||
content = content.replace(/Your arousal/g, 'Your excitement');
|
|
||||||
content = content.replace(/maximize arousal/g, 'maximize pleasure');
|
|
||||||
content = content.replace(/maintaining arousal/g, 'maintaining excitement');
|
|
||||||
content = content.replace(/perfect arousal/g, 'perfect excitement');
|
|
||||||
content = content.replace(/arousal level/g, 'excitement level');
|
|
||||||
content = content.replace(/control is /g, 'focus is ');
|
|
||||||
content = content.replace(/edge control/g, 'edge focus');
|
|
||||||
content = content.replace(/perfect control/g, 'perfect focus');
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, content);
|
|
||||||
console.log(`Completed: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
console.log('Removing arousal/control/intensity effects from game mode files...');
|
|
||||||
|
|
||||||
gameModeFiles.forEach(file => {
|
|
||||||
const fullPath = path.join(process.cwd(), file);
|
|
||||||
if (fs.existsSync(fullPath)) {
|
|
||||||
removeEffectsFromFile(fullPath);
|
|
||||||
} else {
|
|
||||||
console.log(`File not found: ${fullPath}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('All effects removed successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
@ -174,6 +174,13 @@ class TaskChallengeGame {
|
||||||
this.discoverAudio();
|
this.discoverAudio();
|
||||||
this.updateLoadingProgress(90, 'Audio discovery started...');
|
this.updateLoadingProgress(90, 'Audio discovery started...');
|
||||||
|
|
||||||
|
// Load video library
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof loadUnifiedVideoGallery === 'function') {
|
||||||
|
loadUnifiedVideoGallery();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// Check for auto-resume after initialization
|
// Check for auto-resume after initialization
|
||||||
this.checkAutoResume();
|
this.checkAutoResume();
|
||||||
|
|
||||||
|
|
@ -188,7 +195,7 @@ class TaskChallengeGame {
|
||||||
// Initialize overall XP display on home screen
|
// Initialize overall XP display on home screen
|
||||||
this.updateOverallXpDisplay();
|
this.updateOverallXpDisplay();
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 1000);
|
}, 1500); // Increased delay to allow video loading
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoadingOverlay() {
|
showLoadingOverlay() {
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,72 @@ ipcMain.handle('read-video-directory', async (event, dirPath) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recursive video directory scanner
|
||||||
|
ipcMain.handle('read-video-directory-recursive', async (event, dirPath) => {
|
||||||
|
const videoExtensions = ['.mp4', '.webm', '.ogv', '.mov', '.avi', '.mkv', '.m4v', '.3gp', '.flv'];
|
||||||
|
const mimeTypeMap = {
|
||||||
|
'.mp4': 'video/mp4',
|
||||||
|
'.webm': 'video/webm',
|
||||||
|
'.ogv': 'video/ogg',
|
||||||
|
'.mov': 'video/quicktime',
|
||||||
|
'.avi': 'video/x-msvideo',
|
||||||
|
'.mkv': 'video/x-matroska',
|
||||||
|
'.m4v': 'video/mp4',
|
||||||
|
'.3gp': 'video/3gpp',
|
||||||
|
'.flv': 'video/x-flv'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function scanDirectory(currentPath) {
|
||||||
|
let videoFiles = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await fs.readdir(currentPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(currentPath, item.name);
|
||||||
|
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
// Recursively scan subdirectories
|
||||||
|
const subDirVideos = await scanDirectory(itemPath);
|
||||||
|
videoFiles.push(...subDirVideos);
|
||||||
|
} else if (item.isFile()) {
|
||||||
|
const ext = path.extname(item.name).toLowerCase();
|
||||||
|
if (videoExtensions.includes(ext)) {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(itemPath);
|
||||||
|
videoFiles.push({
|
||||||
|
name: item.name,
|
||||||
|
path: itemPath,
|
||||||
|
url: `file:///${itemPath.replace(/\\/g, '/')}`,
|
||||||
|
title: item.name.replace(/\.[^/.]+$/, "").replace(/[-_]/g, ' ').replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()),
|
||||||
|
size: stats.size,
|
||||||
|
type: mimeTypeMap[ext] || 'video/mp4',
|
||||||
|
directory: path.dirname(itemPath)
|
||||||
|
});
|
||||||
|
} catch (statError) {
|
||||||
|
console.warn(`Could not stat video file: ${itemPath}`, statError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (readError) {
|
||||||
|
console.warn(`Could not read directory: ${currentPath}`, readError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Starting recursive scan of: ${dirPath}`);
|
||||||
|
const allVideos = await scanDirectory(dirPath);
|
||||||
|
console.log(`✅ Recursive scan complete: Found ${allVideos.length} videos`);
|
||||||
|
return allVideos;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in recursive video directory scan:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('copy-video', async (event, sourcePath, destPath) => {
|
ipcMain.handle('copy-video', async (event, sourcePath, destPath) => {
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
// Video file operations
|
// Video file operations
|
||||||
selectVideos: () => ipcRenderer.invoke('select-videos'),
|
selectVideos: () => ipcRenderer.invoke('select-videos'),
|
||||||
readVideoDirectory: (dirPath) => ipcRenderer.invoke('read-video-directory', dirPath),
|
readVideoDirectory: (dirPath) => ipcRenderer.invoke('read-video-directory', dirPath),
|
||||||
|
readVideoDirectoryRecursive: (dirPath) => ipcRenderer.invoke('read-video-directory-recursive', dirPath),
|
||||||
copyVideo: (sourcePath, destPath) => ipcRenderer.invoke('copy-video', sourcePath, destPath),
|
copyVideo: (sourcePath, destPath) => ipcRenderer.invoke('copy-video', sourcePath, destPath),
|
||||||
deleteVideo: (filePath) => ipcRenderer.invoke('delete-video', filePath),
|
deleteVideo: (filePath) => ipcRenderer.invoke('delete-video', filePath),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,982 +0,0 @@
|
||||||
/**
|
|
||||||
* Porn Cinema Media Player
|
|
||||||
* Full-featured video player with playlist support and one-handed controls
|
|
||||||
* Now extends BaseVideoPlayer for shared functionality
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PornCinema extends BaseVideoPlayer {
|
|
||||||
constructor() {
|
|
||||||
// Initialize base video player with full features enabled
|
|
||||||
super('#video-container', {
|
|
||||||
showControls: true,
|
|
||||||
autoHide: true,
|
|
||||||
showProgress: true,
|
|
||||||
showVolume: true,
|
|
||||||
showFullscreen: true,
|
|
||||||
showQuality: true,
|
|
||||||
showSpeed: true,
|
|
||||||
keyboardShortcuts: true,
|
|
||||||
minimal: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cinema-specific properties
|
|
||||||
this.shouldAutoPlay = false;
|
|
||||||
this.fallbackMimeTypes = null;
|
|
||||||
this.currentVideoSrc = null;
|
|
||||||
|
|
||||||
// Playlist
|
|
||||||
this.playlist = [];
|
|
||||||
this.currentPlaylistIndex = -1;
|
|
||||||
this.shuffleMode = false;
|
|
||||||
this.originalPlaylistOrder = [];
|
|
||||||
|
|
||||||
// Video library
|
|
||||||
this.videoLibrary = null;
|
|
||||||
|
|
||||||
// Cinema-specific keyboard shortcuts (extend base shortcuts)
|
|
||||||
this.cinemaShortcuts = {
|
|
||||||
'n': () => this.nextVideo(),
|
|
||||||
'N': () => this.nextVideo(),
|
|
||||||
'p': () => this.previousVideo(),
|
|
||||||
'P': () => this.previousVideo(),
|
|
||||||
's': () => this.shufflePlaylist(),
|
|
||||||
'S': () => this.shufflePlaylist(),
|
|
||||||
'1': () => this.setQuality('1080p'),
|
|
||||||
'2': () => this.setQuality('720p'),
|
|
||||||
'3': () => this.setQuality('480p'),
|
|
||||||
'4': () => this.setQuality('360p'),
|
|
||||||
'Enter': () => this.addCurrentToPlaylist()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.initializeCinemaElements();
|
|
||||||
this.attachCinemaEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize cinema-specific elements (extends BaseVideoPlayer initialization)
|
|
||||||
*/
|
|
||||||
initializeCinemaElements() {
|
|
||||||
// Call parent initialization first
|
|
||||||
super.initializeElements();
|
|
||||||
|
|
||||||
// Initialize cinema-specific elements
|
|
||||||
this.initializeCinemaSpecificElements();
|
|
||||||
this.extendCinemaKeyboardShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize cinema-specific UI elements and controls
|
|
||||||
*/
|
|
||||||
initializeCinemaSpecificElements() {
|
|
||||||
// Cinema-specific navigation elements
|
|
||||||
this.controls.prevVideo = document.getElementById('prev-video-btn');
|
|
||||||
this.controls.nextVideo = document.getElementById('next-video-btn');
|
|
||||||
this.controls.addToPlaylist = document.getElementById('add-to-playlist-btn');
|
|
||||||
this.controls.theater = document.getElementById('theater-mode-btn');
|
|
||||||
|
|
||||||
// Playlist management elements
|
|
||||||
this.playlistContent = document.getElementById('playlist-content');
|
|
||||||
this.shuffleBtn = document.getElementById('shuffle-playlist');
|
|
||||||
this.clearPlaylistBtn = document.getElementById('clear-playlist');
|
|
||||||
this.savePlaylistBtn = document.getElementById('save-playlist');
|
|
||||||
this.loadPlaylistBtn = document.getElementById('load-playlist');
|
|
||||||
|
|
||||||
// Cinema-specific display elements
|
|
||||||
this.videoTitle = document.getElementById('video-title');
|
|
||||||
this.videoInfo = document.getElementById('video-info');
|
|
||||||
this.playOverlay = document.getElementById('play-overlay');
|
|
||||||
this.playButtonLarge = document.getElementById('play-button-large');
|
|
||||||
|
|
||||||
console.log('🎬 Cinema-specific elements initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend base keyboard shortcuts with cinema-specific shortcuts
|
|
||||||
*/
|
|
||||||
extendCinemaKeyboardShortcuts() {
|
|
||||||
// Add cinema shortcuts to the base shortcuts
|
|
||||||
Object.assign(this.keyboardShortcuts, this.cinemaShortcuts);
|
|
||||||
console.log('🎹 Cinema keyboard shortcuts extended');
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
console.log('🎬 Initializing Porn Cinema...');
|
|
||||||
|
|
||||||
// Initialize video library
|
|
||||||
this.videoLibrary = new VideoLibrary(this);
|
|
||||||
await this.videoLibrary.loadVideoLibrary();
|
|
||||||
|
|
||||||
// Set initial volume
|
|
||||||
this.setVolume(this.volume);
|
|
||||||
|
|
||||||
console.log('✅ Porn Cinema initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach cinema-specific event listeners (extends BaseVideoPlayer listeners)
|
|
||||||
*/
|
|
||||||
attachCinemaEventListeners() {
|
|
||||||
// Call parent event listeners first
|
|
||||||
super.attachEventListeners();
|
|
||||||
|
|
||||||
// Cinema-specific control buttons
|
|
||||||
if (this.controls.prevVideo) {
|
|
||||||
this.controls.prevVideo.addEventListener('click', () => this.previousVideo());
|
|
||||||
}
|
|
||||||
if (this.controls.nextVideo) {
|
|
||||||
this.controls.nextVideo.addEventListener('click', () => this.nextVideo());
|
|
||||||
}
|
|
||||||
if (this.controls.addToPlaylist) {
|
|
||||||
this.controls.addToPlaylist.addEventListener('click', () => this.addCurrentToPlaylist());
|
|
||||||
}
|
|
||||||
if (this.controls.theater) {
|
|
||||||
this.controls.theater.addEventListener('click', () => this.toggleTheaterMode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playlist management buttons
|
|
||||||
if (this.shuffleBtn) {
|
|
||||||
this.shuffleBtn.addEventListener('click', () => this.shufflePlaylist());
|
|
||||||
}
|
|
||||||
if (this.clearPlaylistBtn) {
|
|
||||||
this.clearPlaylistBtn.addEventListener('click', () => this.clearPlaylist());
|
|
||||||
}
|
|
||||||
if (this.savePlaylistBtn) {
|
|
||||||
this.savePlaylistBtn.addEventListener('click', () => this.savePlaylist());
|
|
||||||
}
|
|
||||||
if (this.loadPlaylistBtn) {
|
|
||||||
this.loadPlaylistBtn.addEventListener('click', () => this.loadPlaylist());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Large play button overlay (cinema-specific)
|
|
||||||
if (this.playButtonLarge) {
|
|
||||||
this.playButtonLarge.addEventListener('click', () => this.togglePlayPause());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override video ended event for playlist functionality
|
|
||||||
if (this.videoElement) {
|
|
||||||
this.videoElement.addEventListener('ended', () => this.onCinemaVideoEnded());
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🎬 Cinema event listeners attached');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle video ended event with playlist progression
|
|
||||||
*/
|
|
||||||
onCinemaVideoEnded() {
|
|
||||||
console.log('🎬 Video ended in cinema mode');
|
|
||||||
|
|
||||||
// Auto-play next video in playlist if available
|
|
||||||
if (this.playlist.length > 0 && this.currentPlaylistIndex < this.playlist.length - 1) {
|
|
||||||
console.log('🎬 Auto-playing next video in playlist');
|
|
||||||
this.nextVideo();
|
|
||||||
} else {
|
|
||||||
// Call parent ended handler
|
|
||||||
super.onEnded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
this.controls.progressBar.addEventListener('click', (e) => this.seekToPosition(e));
|
|
||||||
this.controls.progressBar.addEventListener('mousedown', () => this.startSeeking());
|
|
||||||
|
|
||||||
// Quality and speed controls
|
|
||||||
this.controls.quality.addEventListener('change', (e) => this.setQuality(e.target.value));
|
|
||||||
this.controls.speed.addEventListener('change', (e) => this.setPlaybackRate(e.target.value));
|
|
||||||
|
|
||||||
// Playlist controls
|
|
||||||
this.shuffleBtn.addEventListener('click', () => this.shufflePlaylist());
|
|
||||||
this.clearPlaylistBtn.addEventListener('click', () => this.clearPlaylist());
|
|
||||||
this.savePlaylistBtn.addEventListener('click', () => this.savePlaylist());
|
|
||||||
this.loadPlaylistBtn.addEventListener('click', () => this.loadPlaylist());
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', (e) => this.handleKeyboardShortcut(e));
|
|
||||||
|
|
||||||
// Fullscreen events
|
|
||||||
document.addEventListener('fullscreenchange', () => this.onFullscreenChange());
|
|
||||||
document.addEventListener('webkitfullscreenchange', () => this.onFullscreenChange());
|
|
||||||
document.addEventListener('mozfullscreenchange', () => this.onFullscreenChange());
|
|
||||||
|
|
||||||
// Theater mode buttons
|
|
||||||
document.getElementById('theater-mode').addEventListener('click', () => this.toggleTheaterMode());
|
|
||||||
document.getElementById('fullscreen-toggle').addEventListener('click', () => this.toggleFullscreen());
|
|
||||||
|
|
||||||
// Video container hover for controls
|
|
||||||
this.videoContainer.addEventListener('mouseenter', () => this.showControls());
|
|
||||||
this.videoContainer.addEventListener('mouseleave', () => this.hideControls());
|
|
||||||
this.videoContainer.addEventListener('mousemove', () => this.showControls());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video loading and playback methods
|
|
||||||
selectVideo(video) {
|
|
||||||
this.currentVideo = video;
|
|
||||||
this.updateVideoInfo();
|
|
||||||
this.updateVideoTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
async playVideo(video) {
|
|
||||||
try {
|
|
||||||
this.currentVideo = video;
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// Convert path to proper format for Electron
|
|
||||||
let videoSrc = video.path;
|
|
||||||
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
|
||||||
// Absolute Windows path - convert to file:// URL
|
|
||||||
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
|
||||||
} else if (window.electronAPI && !video.path.startsWith('file://')) {
|
|
||||||
// Relative path in Electron - use as is
|
|
||||||
videoSrc = video.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if browser can play this format
|
|
||||||
const canPlayResult = this.checkCanPlay(video.format || video.path);
|
|
||||||
console.log(`🎬 Playing ${video.name} (${video.format}) - Can play: ${canPlayResult.canPlay ? 'Yes' : 'No'}`);
|
|
||||||
|
|
||||||
// For .mov files, try multiple MIME types as fallback
|
|
||||||
const mimeTypes = this.getVideoMimeTypes(video.format || video.path);
|
|
||||||
console.log(`🎬 Trying MIME types: ${mimeTypes.join(', ')}`);
|
|
||||||
|
|
||||||
// Update video source - try primary MIME type first
|
|
||||||
this.videoSource.src = videoSrc;
|
|
||||||
this.videoSource.type = canPlayResult.mimeType || mimeTypes[0];
|
|
||||||
|
|
||||||
// Store fallback MIME types for error handling
|
|
||||||
this.fallbackMimeTypes = mimeTypes.slice(1);
|
|
||||||
this.currentVideoSrc = videoSrc;
|
|
||||||
|
|
||||||
// Set flag to auto-play when loaded
|
|
||||||
this.shouldAutoPlay = true;
|
|
||||||
|
|
||||||
// Load the video
|
|
||||||
this.videoElement.load();
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
this.updateVideoInfo();
|
|
||||||
this.updateVideoTitle();
|
|
||||||
this.updatePlaylistSelection();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error playing video:', error);
|
|
||||||
this.showError('Failed to load video');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCanPlay(formatOrPath) {
|
|
||||||
let extension = formatOrPath;
|
|
||||||
if (formatOrPath.includes('.')) {
|
|
||||||
extension = formatOrPath.toLowerCase().split('.').pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const testVideo = document.createElement('video');
|
|
||||||
const mimeTypes = this.getVideoMimeTypes(formatOrPath);
|
|
||||||
|
|
||||||
for (const mimeType of mimeTypes) {
|
|
||||||
const canPlay = testVideo.canPlayType(mimeType);
|
|
||||||
if (canPlay === 'probably' || canPlay === 'maybe') {
|
|
||||||
return { canPlay: true, mimeType };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { canPlay: false, mimeType: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoMimeTypes(formatOrPath) {
|
|
||||||
let extension = formatOrPath;
|
|
||||||
if (formatOrPath.includes('.')) {
|
|
||||||
extension = formatOrPath.toLowerCase().split('.').pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (extension.toLowerCase()) {
|
|
||||||
case 'mp4':
|
|
||||||
return ['video/mp4'];
|
|
||||||
case 'webm':
|
|
||||||
return ['video/webm'];
|
|
||||||
case 'mov':
|
|
||||||
case 'qt':
|
|
||||||
// Try multiple MIME types for .mov files
|
|
||||||
return ['video/quicktime', 'video/mp4', 'video/x-quicktime'];
|
|
||||||
case 'avi':
|
|
||||||
return ['video/avi', 'video/x-msvideo'];
|
|
||||||
case 'mkv':
|
|
||||||
return ['video/x-matroska'];
|
|
||||||
case 'ogg':
|
|
||||||
return ['video/ogg'];
|
|
||||||
case 'm4v':
|
|
||||||
return ['video/mp4'];
|
|
||||||
default:
|
|
||||||
return ['video/mp4']; // Default fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePlayPause() {
|
|
||||||
if (!this.videoElement.src) {
|
|
||||||
// No video loaded, try to play first video from library or playlist
|
|
||||||
this.playFirstAvailable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.videoElement.paused) {
|
|
||||||
this.play();
|
|
||||||
} else {
|
|
||||||
this.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
play() {
|
|
||||||
const playPromise = this.videoElement.play();
|
|
||||||
|
|
||||||
if (playPromise !== undefined) {
|
|
||||||
playPromise.then(() => {
|
|
||||||
this.isPlaying = true;
|
|
||||||
this.updatePlayButton();
|
|
||||||
this.hidePlayOverlay();
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Error playing video:', error);
|
|
||||||
this.showError('Failed to play video');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this.videoElement.pause();
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.updatePlayButton();
|
|
||||||
this.showPlayOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
seek(seconds) {
|
|
||||||
if (this.videoElement.duration) {
|
|
||||||
const newTime = Math.max(0, Math.min(this.videoElement.duration, this.videoElement.currentTime + seconds));
|
|
||||||
this.videoElement.currentTime = newTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seekToPosition(event) {
|
|
||||||
if (this.videoElement.duration) {
|
|
||||||
const rect = this.controls.progressBar.getBoundingClientRect();
|
|
||||||
const pos = (event.clientX - rect.left) / rect.width;
|
|
||||||
this.videoElement.currentTime = pos * this.videoElement.duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume and audio controls
|
|
||||||
setVolume(volume) {
|
|
||||||
this.volume = Math.max(0, Math.min(1, volume));
|
|
||||||
this.videoElement.volume = this.volume;
|
|
||||||
this.controls.volume.value = this.volume * 100;
|
|
||||||
this.controls.volumePercentage.textContent = Math.round(this.volume * 100) + '%';
|
|
||||||
this.updateMuteButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustVolume(delta) {
|
|
||||||
this.setVolume(this.volume + delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMute() {
|
|
||||||
if (this.videoElement.muted) {
|
|
||||||
this.videoElement.muted = false;
|
|
||||||
this.setVolume(this.volume > 0 ? this.volume : 0.7);
|
|
||||||
} else {
|
|
||||||
this.videoElement.muted = true;
|
|
||||||
}
|
|
||||||
this.updateMuteButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMuteButton() {
|
|
||||||
const isMuted = this.videoElement.muted || this.volume === 0;
|
|
||||||
this.controls.mute.textContent = isMuted ? '🔇' : '🔊';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quality and playback controls
|
|
||||||
setQuality(quality) {
|
|
||||||
this.currentQuality = quality;
|
|
||||||
this.controls.quality.value = quality;
|
|
||||||
|
|
||||||
// In a real implementation, you would switch video sources here
|
|
||||||
// For now, we'll just update the UI
|
|
||||||
console.log(`Quality set to: ${quality}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlaybackRate(rate) {
|
|
||||||
this.playbackRate = parseFloat(rate);
|
|
||||||
this.videoElement.playbackRate = this.playbackRate;
|
|
||||||
this.controls.speed.value = rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fullscreen and theater mode
|
|
||||||
toggleFullscreen() {
|
|
||||||
if (!this.isFullscreen) {
|
|
||||||
this.enterFullscreen();
|
|
||||||
} else {
|
|
||||||
this.exitFullscreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enterFullscreen() {
|
|
||||||
const element = this.videoContainer;
|
|
||||||
|
|
||||||
if (element.requestFullscreen) {
|
|
||||||
element.requestFullscreen();
|
|
||||||
} else if (element.webkitRequestFullscreen) {
|
|
||||||
element.webkitRequestFullscreen();
|
|
||||||
} else if (element.mozRequestFullScreen) {
|
|
||||||
element.mozRequestFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exitFullscreen() {
|
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else if (document.webkitExitFullscreen) {
|
|
||||||
document.webkitExitFullscreen();
|
|
||||||
} else if (document.mozCancelFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTheaterMode() {
|
|
||||||
this.theaterMode = !this.theaterMode;
|
|
||||||
document.body.classList.toggle('theater-mode', this.theaterMode);
|
|
||||||
|
|
||||||
const button = document.getElementById('theater-mode');
|
|
||||||
if (button) {
|
|
||||||
button.textContent = this.theaterMode ? '💡' : '🎭';
|
|
||||||
button.title = this.theaterMode ? 'Exit Theater Mode' : 'Theater Mode (Dim UI)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playlist management
|
|
||||||
async addToPlaylist(video) {
|
|
||||||
if (!this.playlist.find(v => v.path === video.path)) {
|
|
||||||
// Create a copy of the video with duration loaded if missing
|
|
||||||
const videoWithDuration = {...video};
|
|
||||||
|
|
||||||
// If duration is missing or 0, try to load it
|
|
||||||
if (!videoWithDuration.duration || videoWithDuration.duration === 0) {
|
|
||||||
try {
|
|
||||||
videoWithDuration.duration = await this.getVideoDuration(video);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Could not load duration for ${video.name}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playlist.push(videoWithDuration);
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
console.log(`➕ Added to playlist: ${video.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addCurrentToPlaylist() {
|
|
||||||
if (this.currentVideo) {
|
|
||||||
await this.addToPlaylist(this.currentVideo);
|
|
||||||
} else if (this.videoLibrary && this.videoLibrary.getSelectedVideo()) {
|
|
||||||
await this.addToPlaylist(this.videoLibrary.getSelectedVideo());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromPlaylist(index) {
|
|
||||||
if (index >= 0 && index < this.playlist.length) {
|
|
||||||
const removed = this.playlist.splice(index, 1)[0];
|
|
||||||
console.log(`➖ Removed from playlist: ${removed.name}`);
|
|
||||||
|
|
||||||
// Adjust current index if necessary
|
|
||||||
if (this.currentPlaylistIndex > index) {
|
|
||||||
this.currentPlaylistIndex--;
|
|
||||||
} else if (this.currentPlaylistIndex === index) {
|
|
||||||
this.currentPlaylistIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearPlaylist() {
|
|
||||||
if (confirm('Clear entire playlist?')) {
|
|
||||||
this.playlist = [];
|
|
||||||
this.currentPlaylistIndex = -1;
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
console.log('🗑️ Playlist cleared');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shufflePlaylist() {
|
|
||||||
if (this.playlist.length <= 1) return;
|
|
||||||
|
|
||||||
this.shuffleMode = !this.shuffleMode;
|
|
||||||
|
|
||||||
if (this.shuffleMode) {
|
|
||||||
// Save original order
|
|
||||||
this.originalPlaylistOrder = [...this.playlist];
|
|
||||||
|
|
||||||
// Shuffle playlist
|
|
||||||
for (let i = this.playlist.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[this.playlist[i], this.playlist[j]] = [this.playlist[j], this.playlist[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shuffleBtn.textContent = '🔀';
|
|
||||||
this.shuffleBtn.title = 'Disable Shuffle';
|
|
||||||
console.log('🔀 Playlist shuffled');
|
|
||||||
} else {
|
|
||||||
// Restore original order
|
|
||||||
this.playlist = [...this.originalPlaylistOrder];
|
|
||||||
this.shuffleBtn.textContent = '🔀';
|
|
||||||
this.shuffleBtn.title = 'Shuffle Playlist';
|
|
||||||
console.log('📝 Playlist order restored');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlaylistIndex = this.playlist.findIndex(v =>
|
|
||||||
this.currentVideo && v.path === this.currentVideo.path
|
|
||||||
);
|
|
||||||
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
nextVideo() {
|
|
||||||
if (this.playlist.length === 0) return;
|
|
||||||
|
|
||||||
let nextIndex = this.currentPlaylistIndex + 1;
|
|
||||||
if (nextIndex >= this.playlist.length) {
|
|
||||||
nextIndex = 0; // Loop back to start
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playVideoFromPlaylist(nextIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousVideo() {
|
|
||||||
if (this.playlist.length === 0) return;
|
|
||||||
|
|
||||||
let prevIndex = this.currentPlaylistIndex - 1;
|
|
||||||
if (prevIndex < 0) {
|
|
||||||
prevIndex = this.playlist.length - 1; // Loop to end
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playVideoFromPlaylist(prevIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
playVideoFromPlaylist(index) {
|
|
||||||
if (index >= 0 && index < this.playlist.length) {
|
|
||||||
this.currentPlaylistIndex = index;
|
|
||||||
this.playVideo(this.playlist[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlaylistDisplay() {
|
|
||||||
if (this.playlist.length === 0) {
|
|
||||||
this.playlistContent.innerHTML = `
|
|
||||||
<div class="playlist-empty">
|
|
||||||
<p>Playlist is empty. Add videos by clicking ➕ or pressing Enter while a video is selected.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playlistContent.innerHTML = this.playlist.map((video, index) => `
|
|
||||||
<div class="playlist-item ${index === this.currentPlaylistIndex ? 'current' : ''}"
|
|
||||||
data-index="${index}">
|
|
||||||
<div class="playlist-thumbnail">
|
|
||||||
${video.thumbnail ?
|
|
||||||
`<img src="${video.thumbnail}" alt="${video.name}" class="playlist-thumbnail">` :
|
|
||||||
`<div class="playlist-thumbnail" style="display: flex; align-items: center; justify-content: center; font-size: 1.2rem;">🎬</div>`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="playlist-details">
|
|
||||||
<div class="playlist-title">${video.name}</div>
|
|
||||||
<div class="playlist-duration">${this.formatDuration(video.duration)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="playlist-actions">
|
|
||||||
<button class="btn btn-mini play-playlist-item" title="Play">▶</button>
|
|
||||||
<button class="btn btn-mini remove-playlist-item" title="Remove">❌</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Attach playlist item events
|
|
||||||
this.attachPlaylistEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachPlaylistEvents() {
|
|
||||||
const playlistItems = this.playlistContent.querySelectorAll('.playlist-item');
|
|
||||||
playlistItems.forEach((item, index) => {
|
|
||||||
item.addEventListener('click', (e) => {
|
|
||||||
if (e.target.closest('.playlist-actions')) return;
|
|
||||||
this.playVideoFromPlaylist(index);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const playButtons = this.playlistContent.querySelectorAll('.play-playlist-item');
|
|
||||||
playButtons.forEach((button, index) => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.playVideoFromPlaylist(index);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeButtons = this.playlistContent.querySelectorAll('.remove-playlist-item');
|
|
||||||
removeButtons.forEach((button, index) => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.removeFromPlaylist(index);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlaylistSelection() {
|
|
||||||
if (this.currentVideo) {
|
|
||||||
this.currentPlaylistIndex = this.playlist.findIndex(v => v.path === this.currentVideo.path);
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
handleKeyboardShortcut(event) {
|
|
||||||
// Don't handle shortcuts if typing in an input
|
|
||||||
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = event.key;
|
|
||||||
if (this.shortcuts[key]) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.shortcuts[key]();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video event handlers
|
|
||||||
onLoadStart() {
|
|
||||||
this.showLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadedMetadata() {
|
|
||||||
this.updateTimeDisplay();
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadedData() {
|
|
||||||
this.hideLoading();
|
|
||||||
|
|
||||||
// Auto-play if requested
|
|
||||||
if (this.shouldAutoPlay) {
|
|
||||||
this.shouldAutoPlay = false;
|
|
||||||
this.videoElement.play().catch(error => {
|
|
||||||
console.error('🎬 Error auto-playing video:', error);
|
|
||||||
this.showError('Failed to play video');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCanPlay() {
|
|
||||||
this.hideLoading();
|
|
||||||
this.updatePlayButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlay() {
|
|
||||||
this.isPlaying = true;
|
|
||||||
this.updatePlayButton();
|
|
||||||
this.hidePlayOverlay();
|
|
||||||
this.hideVideoOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPause() {
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.updatePlayButton();
|
|
||||||
this.showPlayOverlay();
|
|
||||||
this.showVideoOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnded() {
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.updatePlayButton();
|
|
||||||
this.showPlayOverlay();
|
|
||||||
this.showVideoOverlay();
|
|
||||||
|
|
||||||
// Auto-play next video in playlist
|
|
||||||
if (this.playlist.length > 0 && this.currentPlaylistIndex >= 0) {
|
|
||||||
setTimeout(() => this.nextVideo(), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTimeUpdate() {
|
|
||||||
this.updateProgressBar();
|
|
||||||
this.updateTimeDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
onVolumeChange() {
|
|
||||||
this.updateMuteButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
onError(event) {
|
|
||||||
const video = event.target;
|
|
||||||
const error = video.error;
|
|
||||||
|
|
||||||
// Try fallback MIME types for .mov files
|
|
||||||
if (this.fallbackMimeTypes && this.fallbackMimeTypes.length > 0) {
|
|
||||||
const nextMimeType = this.fallbackMimeTypes.shift();
|
|
||||||
console.log(`🔄 Trying fallback MIME type: ${nextMimeType}`);
|
|
||||||
|
|
||||||
this.videoSource.type = nextMimeType;
|
|
||||||
this.videoElement.load();
|
|
||||||
return; // Don't show error yet, try the fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorMessage = 'Error loading video';
|
|
||||||
let detailedMessage = 'Unknown error';
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
switch (error.code) {
|
|
||||||
case error.MEDIA_ERR_ABORTED:
|
|
||||||
detailedMessage = 'Video loading was aborted';
|
|
||||||
break;
|
|
||||||
case error.MEDIA_ERR_NETWORK:
|
|
||||||
detailedMessage = 'Network error while loading video';
|
|
||||||
break;
|
|
||||||
case error.MEDIA_ERR_DECODE:
|
|
||||||
detailedMessage = 'Video format not supported or corrupted';
|
|
||||||
errorMessage = 'Unsupported video format';
|
|
||||||
break;
|
|
||||||
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
||||||
detailedMessage = 'Video format or codec not supported';
|
|
||||||
errorMessage = 'Video format not supported';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
detailedMessage = `Unknown error (code: ${error.code})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`🎬 Video error for ${this.currentVideo?.name || 'unknown'}:`, {
|
|
||||||
code: error?.code,
|
|
||||||
message: error?.message,
|
|
||||||
details: detailedMessage,
|
|
||||||
format: this.currentVideo?.format,
|
|
||||||
src: video.src,
|
|
||||||
mimeType: this.videoSource.type
|
|
||||||
});
|
|
||||||
|
|
||||||
this.hideLoading();
|
|
||||||
this.showError(errorMessage);
|
|
||||||
|
|
||||||
// Reset fallback MIME types
|
|
||||||
this.fallbackMimeTypes = null;
|
|
||||||
|
|
||||||
// If it's a .mov file that failed, suggest alternatives
|
|
||||||
if (this.currentVideo?.format?.toLowerCase() === 'mov') {
|
|
||||||
console.warn('💡 .mov file failed to play. This format may contain codecs not supported by browsers.');
|
|
||||||
console.warn('💡 Consider converting .mov files to .mp4 for better compatibility.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFullscreenChange() {
|
|
||||||
this.isFullscreen = !!(document.fullscreenElement ||
|
|
||||||
document.webkitFullscreenElement ||
|
|
||||||
document.mozFullScreenElement);
|
|
||||||
|
|
||||||
this.controls.fullscreen.textContent = this.isFullscreen ? '🗗' : '⛶';
|
|
||||||
this.controls.fullscreen.title = this.isFullscreen ? 'Exit Fullscreen (Esc)' : 'Fullscreen (F)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI update methods
|
|
||||||
updateVideoInfo() {
|
|
||||||
if (this.currentVideo) {
|
|
||||||
const info = `${this.currentVideo.resolution || 'Unknown'} • ${this.formatFileSize(this.currentVideo.size)}`;
|
|
||||||
this.videoInfo.textContent = info;
|
|
||||||
} else {
|
|
||||||
this.videoInfo.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVideoTitle() {
|
|
||||||
if (this.currentVideo) {
|
|
||||||
this.videoTitle.textContent = this.currentVideo.name;
|
|
||||||
} else {
|
|
||||||
this.videoTitle.textContent = 'Select a video to begin';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayButton() {
|
|
||||||
const playText = this.isPlaying ? '⏸' : '▶';
|
|
||||||
this.controls.playPause.textContent = playText;
|
|
||||||
this.playButtonLarge.textContent = this.isPlaying ? '⏸' : '▶';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgressBar() {
|
|
||||||
if (this.videoElement.duration) {
|
|
||||||
const progress = (this.videoElement.currentTime / this.videoElement.duration) * 100;
|
|
||||||
this.controls.progressFilled.style.width = progress + '%';
|
|
||||||
this.controls.progressThumb.style.left = progress + '%';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimeDisplay() {
|
|
||||||
this.controls.currentTime.textContent = this.formatDuration(this.videoElement.currentTime);
|
|
||||||
this.controls.totalTime.textContent = this.formatDuration(this.videoElement.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI state methods
|
|
||||||
showLoading() {
|
|
||||||
this.videoLoading.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoading() {
|
|
||||||
this.videoLoading.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
showPlayOverlay() {
|
|
||||||
this.playOverlay.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
hidePlayOverlay() {
|
|
||||||
this.playOverlay.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
showVideoOverlay() {
|
|
||||||
this.videoOverlay.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
hideVideoOverlay() {
|
|
||||||
this.videoOverlay.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
showControls() {
|
|
||||||
this.controls.container.classList.add('visible');
|
|
||||||
this.videoContainer.classList.remove('hide-controls', 'auto-hide');
|
|
||||||
|
|
||||||
// Clear existing timeout
|
|
||||||
if (this.hideControlsTimeout) {
|
|
||||||
clearTimeout(this.hideControlsTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set timeout to auto-hide controls after 3 seconds of no interaction
|
|
||||||
if (this.videoElement && !this.videoElement.paused) {
|
|
||||||
this.hideControlsTimeout = setTimeout(() => {
|
|
||||||
this.videoContainer.classList.add('auto-hide');
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideControls() {
|
|
||||||
if (!this.videoElement.paused) {
|
|
||||||
this.videoContainer.classList.add('auto-hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.hideLoading();
|
|
||||||
this.videoTitle.textContent = message;
|
|
||||||
this.videoInfo.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
playFirstAvailable() {
|
|
||||||
if (this.playlist.length > 0) {
|
|
||||||
this.playVideoFromPlaylist(0);
|
|
||||||
} else if (this.videoLibrary && this.videoLibrary.getFilteredVideos().length > 0) {
|
|
||||||
this.playVideo(this.videoLibrary.getFilteredVideos()[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startSeeking() {
|
|
||||||
// Add mouse move listener for seeking
|
|
||||||
// This would be implemented for smooth seeking while dragging
|
|
||||||
}
|
|
||||||
|
|
||||||
savePlaylist() {
|
|
||||||
if (this.playlist.length === 0) {
|
|
||||||
alert('Playlist is empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playlistData = {
|
|
||||||
name: prompt('Enter playlist name:') || 'Untitled Playlist',
|
|
||||||
videos: this.playlist,
|
|
||||||
created: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save to localStorage (in a real app, you might save to a file)
|
|
||||||
const savedPlaylists = JSON.parse(localStorage.getItem('pornCinemaPlaylists') || '[]');
|
|
||||||
savedPlaylists.push(playlistData);
|
|
||||||
localStorage.setItem('pornCinemaPlaylists', JSON.stringify(savedPlaylists));
|
|
||||||
|
|
||||||
console.log(`💾 Playlist saved: ${playlistData.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPlaylist() {
|
|
||||||
const savedPlaylists = JSON.parse(localStorage.getItem('pornCinemaPlaylists') || '[]');
|
|
||||||
|
|
||||||
if (savedPlaylists.length === 0) {
|
|
||||||
alert('No saved playlists found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a simple selection dialog
|
|
||||||
const playlistNames = savedPlaylists.map((p, i) => `${i + 1}. ${p.name}`).join('\n');
|
|
||||||
const selection = prompt(`Select playlist to load:\n${playlistNames}\n\nEnter number:`);
|
|
||||||
|
|
||||||
if (selection) {
|
|
||||||
const index = parseInt(selection) - 1;
|
|
||||||
if (index >= 0 && index < savedPlaylists.length) {
|
|
||||||
this.playlist = savedPlaylists[index].videos;
|
|
||||||
this.currentPlaylistIndex = -1;
|
|
||||||
this.updatePlaylistDisplay();
|
|
||||||
console.log(`📁 Playlist loaded: ${savedPlaylists[index].name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getVideoDuration(video) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Create a temporary video element to load metadata
|
|
||||||
const tempVideo = document.createElement('video');
|
|
||||||
tempVideo.preload = 'metadata';
|
|
||||||
|
|
||||||
// Convert path to proper format for Electron
|
|
||||||
let videoSrc = video.path;
|
|
||||||
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
|
||||||
// Absolute Windows path - convert to file:// URL
|
|
||||||
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempVideo.addEventListener('loadedmetadata', () => {
|
|
||||||
const duration = tempVideo.duration;
|
|
||||||
tempVideo.remove(); // Clean up
|
|
||||||
resolve(duration);
|
|
||||||
});
|
|
||||||
|
|
||||||
tempVideo.addEventListener('error', (e) => {
|
|
||||||
tempVideo.remove(); // Clean up
|
|
||||||
reject(new Error(`Failed to load video metadata: ${e.message}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set timeout to avoid hanging
|
|
||||||
setTimeout(() => {
|
|
||||||
tempVideo.remove();
|
|
||||||
reject(new Error('Timeout loading video metadata'));
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
tempVideo.src = videoSrc;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDuration(seconds) {
|
|
||||||
if (!seconds || isNaN(seconds)) return '0:00';
|
|
||||||
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
} else {
|
|
||||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFileSize(bytes) {
|
|
||||||
if (!bytes || bytes === 0) return '0 B';
|
|
||||||
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -132,9 +132,6 @@ class VideoLibrary {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`📁 Loaded ${this.videos.length} videos`);
|
console.log(`📁 Loaded ${this.videos.length} videos`);
|
||||||
if (this.videos.length > 0) {
|
|
||||||
console.log('📁 Sample video object:', this.videos[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply current filters and display
|
// Apply current filters and display
|
||||||
this.applyFiltersAndSort();
|
this.applyFiltersAndSort();
|
||||||
|
|
|
||||||
|
|
@ -1161,10 +1161,11 @@ class InteractiveTaskManager {
|
||||||
<label for="focus-video-volume" style="color: #bbb; font-size: 10px;">🔊</label>
|
<label for="focus-video-volume" style="color: #bbb; font-size: 10px;">🔊</label>
|
||||||
<input type="range"
|
<input type="range"
|
||||||
id="focus-video-volume"
|
id="focus-video-volume"
|
||||||
|
class="focus-volume-slider"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
value="50"
|
value="50"
|
||||||
style="width: 60px; height: 3px; accent-color: #673ab7;">
|
style="width: 60px !important; max-width: 60px !important; min-width: 60px !important; height: 3px !important; accent-color: #673ab7;">
|
||||||
<span id="focus-volume-display" style="color: #bbb; font-size: 9px; min-width: 20px;">50%</span>
|
<span id="focus-volume-display" style="color: #bbb; font-size: 9px; min-width: 20px;">50%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-info" id="video-info" style="font-size: 12px; color: #bbb; margin-top: 5px; text-align: center;">
|
<div class="video-info" id="video-info" style="font-size: 12px; color: #bbb; margin-top: 5px; text-align: center;">
|
||||||
|
|
|
||||||
|
|
@ -148,24 +148,21 @@ class VideoPlayerManager {
|
||||||
try {
|
try {
|
||||||
// In Electron environment, use the desktop file manager
|
// In Electron environment, use the desktop file manager
|
||||||
if (window.electronAPI && window.desktopFileManager) {
|
if (window.electronAPI && window.desktopFileManager) {
|
||||||
// Map category names to match file manager
|
// Use unified video library - get all videos and filter by category if needed
|
||||||
const dirCategory = category === 'task' ? 'tasks' :
|
const allVideos = window.desktopFileManager.getAllVideos();
|
||||||
category === 'reward' ? 'rewards' :
|
|
||||||
category === 'punishment' ? 'punishments' : category;
|
// For now, just use all videos for any category since we removed categories
|
||||||
const videos = await window.desktopFileManager.scanDirectoryForVideos(dirCategory);
|
// In the future, we could add tags or other filtering mechanisms
|
||||||
this.videoLibrary[category] = videos;
|
this.videoLibrary[category] = allVideos;
|
||||||
} else {
|
} else {
|
||||||
// In browser environment, try to load from localStorage
|
// In browser environment, try to load from unified storage
|
||||||
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
|
||||||
const dirCategory = category === 'task' ? 'tasks' :
|
this.videoLibrary[category] = unifiedData.allVideos || [];
|
||||||
category === 'reward' ? 'rewards' :
|
|
||||||
category === 'punishment' ? 'punishments' : category;
|
|
||||||
this.videoLibrary[category] = storedVideos[dirCategory] || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📹 Found ${this.videoLibrary[category].length} ${category} videos`);
|
console.log(`📹 Found ${this.videoLibrary[category].length} ${category} videos (from unified library)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`⚠️ Could not scan ${category} videos:`, error);
|
console.warn(`⚠️ Could not load ${category} videos:`, error);
|
||||||
this.videoLibrary[category] = [];
|
this.videoLibrary[category] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2289,12 +2289,35 @@ body.theme-monochrome {
|
||||||
/* Video Management Styles */
|
/* Video Management Styles */
|
||||||
.video-gallery {
|
.video-gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
grid-auto-rows: minmax(320px, auto);
|
||||||
gap: var(--space-lg);
|
gap: var(--space-lg);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
align-content: start;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling for video gallery */
|
||||||
|
.video-gallery::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-gallery::-webkit-scrollbar-track {
|
||||||
|
background: #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-gallery::-webkit-scrollbar-thumb {
|
||||||
|
background: #666;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-gallery::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-gallery.active {
|
.video-gallery.active {
|
||||||
|
|
@ -2309,6 +2332,7 @@ body.theme-monochrome {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item:hover {
|
.video-item:hover {
|
||||||
|
|
@ -2326,9 +2350,10 @@ body.theme-monochrome {
|
||||||
.video-thumbnail {
|
.video-thumbnail {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 120px;
|
height: 140px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-thumbnail video {
|
.video-thumbnail video {
|
||||||
|
|
@ -3400,6 +3425,56 @@ body.theme-monochrome {
|
||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Focus video player volume slider override */
|
||||||
|
.focus-volume-slider {
|
||||||
|
width: 60px !important;
|
||||||
|
max-width: 60px !important;
|
||||||
|
min-width: 60px !important;
|
||||||
|
height: 3px !important;
|
||||||
|
background: #666 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance optimizations for large video galleries */
|
||||||
|
.video-galleries-container {
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unified-video-gallery {
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item {
|
||||||
|
transform: translateZ(0); /* Force hardware acceleration */
|
||||||
|
will-change: transform; /* Hint to browser for optimization */
|
||||||
|
contain: layout style paint; /* CSS containment for better performance */
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-thumbnail {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-placeholder {
|
||||||
|
font-size: 24px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Virtual scrolling optimization */
|
||||||
|
.video-gallery.active {
|
||||||
|
contain: strict; /* Strict containment for performance */
|
||||||
|
}
|
||||||
|
|
||||||
.control-group input[type="checkbox"] {
|
.control-group input[type="checkbox"] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ class DesktopFileManager {
|
||||||
punishments: null
|
punishments: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// External video directories (linked, not copied)
|
||||||
|
this.externalVideoDirectories = []; // Array of linked directory objects
|
||||||
|
this.allLinkedVideos = []; // Cached array of all videos from all directories
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,15 +51,16 @@ class DesktopFileManager {
|
||||||
await window.electronAPI.createDirectory(this.audioDirectories.background);
|
await window.electronAPI.createDirectory(this.audioDirectories.background);
|
||||||
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
|
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
|
||||||
|
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.background);
|
// Note: No longer creating/using local video directories
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.tasks);
|
// All videos come from external linked directories only
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.rewards);
|
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.punishments);
|
|
||||||
|
|
||||||
console.log('Desktop file manager initialized');
|
console.log('Desktop file manager initialized');
|
||||||
console.log('App path:', this.appPath);
|
console.log('App path:', this.appPath);
|
||||||
console.log('Image directories:', this.imageDirectories);
|
console.log('Image directories:', this.imageDirectories);
|
||||||
console.log('Audio directories:', this.audioDirectories);
|
console.log('Audio directories:', this.audioDirectories);
|
||||||
|
|
||||||
|
// Load any previously linked external directories
|
||||||
|
await this.loadLinkedDirectories();
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to get app path');
|
console.error('Failed to get app path');
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +281,282 @@ class DesktopFileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addVideoDirectory(customName = null) {
|
||||||
|
if (!this.isElectron) {
|
||||||
|
this.showNotification('Directory linking only available in desktop version', 'warning');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Open directory dialog
|
||||||
|
const directoryPath = await window.electronAPI.selectDirectory();
|
||||||
|
|
||||||
|
if (!directoryPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if directory is already linked
|
||||||
|
const existingDir = this.externalVideoDirectories.find(dir => dir.path === directoryPath);
|
||||||
|
if (existingDir) {
|
||||||
|
this.showNotification('Directory is already linked!', 'warning');
|
||||||
|
return existingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show scanning notification
|
||||||
|
this.showNotification('🔍 Scanning directory for videos... This may take a moment for large collections.', 'info');
|
||||||
|
|
||||||
|
// Scan the directory recursively for video files
|
||||||
|
console.log(`🔍 Scanning directory recursively: ${directoryPath}`);
|
||||||
|
const videoFiles = await window.electronAPI.readVideoDirectoryRecursive(directoryPath);
|
||||||
|
|
||||||
|
console.log(`Found ${videoFiles.length} video files in directory:`, directoryPath);
|
||||||
|
|
||||||
|
if (videoFiles.length === 0) {
|
||||||
|
this.showNotification('No video files found in selected directory', 'warning');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory object
|
||||||
|
const directoryName = customName || this.getDirectoryName(directoryPath);
|
||||||
|
const directoryObj = {
|
||||||
|
id: Date.now(), // Unique ID
|
||||||
|
name: directoryName,
|
||||||
|
path: directoryPath,
|
||||||
|
videoCount: videoFiles.length,
|
||||||
|
dateAdded: new Date().toISOString(),
|
||||||
|
isRecursive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process videos in chunks to avoid blocking UI
|
||||||
|
const linkedVideos = await this.processVideosInChunks(videoFiles, directoryObj);
|
||||||
|
|
||||||
|
// Add to linked directories
|
||||||
|
this.externalVideoDirectories.push(directoryObj);
|
||||||
|
|
||||||
|
// Update cached video list
|
||||||
|
this.allLinkedVideos.push(...linkedVideos);
|
||||||
|
|
||||||
|
// Save to persistent storage
|
||||||
|
await this.saveLinkedDirectories();
|
||||||
|
|
||||||
|
// Update video storage
|
||||||
|
await this.updateUnifiedVideoStorage();
|
||||||
|
|
||||||
|
this.showNotification(
|
||||||
|
`✅ Linked directory "${directoryName}"!\nFound ${videoFiles.length} videos`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🔗 Linked directory: ${directoryName} (${videoFiles.length} videos)`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
directory: directoryObj,
|
||||||
|
videoCount: videoFiles.length,
|
||||||
|
videos: linkedVideos
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error linking video directory:', error);
|
||||||
|
this.showNotification('Failed to link video directory', 'error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processVideosInChunks(videoFiles, directoryObj) {
|
||||||
|
const chunkSize = 100; // Process 100 videos at a time
|
||||||
|
const linkedVideos = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < videoFiles.length; i += chunkSize) {
|
||||||
|
const chunk = videoFiles.slice(i, i + chunkSize);
|
||||||
|
const progress = Math.round(((i + chunk.length) / videoFiles.length) * 100);
|
||||||
|
|
||||||
|
console.log(`Processing videos ${i + 1}-${i + chunk.length} of ${videoFiles.length} (${progress}%)`);
|
||||||
|
|
||||||
|
const chunkProcessed = chunk.map(video => ({
|
||||||
|
id: `${directoryObj.id}_${video.name}`,
|
||||||
|
name: video.name,
|
||||||
|
path: video.path,
|
||||||
|
url: `file:///${video.path.replace(/\\/g, '/')}`,
|
||||||
|
title: this.getVideoTitle(video.name),
|
||||||
|
size: video.size || 0,
|
||||||
|
directoryId: directoryObj.id,
|
||||||
|
directoryName: directoryObj.name,
|
||||||
|
isExternal: true,
|
||||||
|
relativePath: video.path.replace(directoryObj.path, '').replace(/^[\\/]/, '')
|
||||||
|
}));
|
||||||
|
|
||||||
|
linkedVideos.push(...chunkProcessed);
|
||||||
|
|
||||||
|
// Small delay to keep UI responsive
|
||||||
|
if (i + chunkSize < videoFiles.length) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return linkedVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeVideoDirectory(directoryId) {
|
||||||
|
if (!this.isElectron) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find directory
|
||||||
|
const dirIndex = this.externalVideoDirectories.findIndex(dir => dir.id === directoryId);
|
||||||
|
if (dirIndex === -1) {
|
||||||
|
this.showNotification('Directory not found', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory = this.externalVideoDirectories[dirIndex];
|
||||||
|
|
||||||
|
// Remove from arrays
|
||||||
|
this.externalVideoDirectories.splice(dirIndex, 1);
|
||||||
|
this.allLinkedVideos = this.allLinkedVideos.filter(video => video.directoryId !== directoryId);
|
||||||
|
|
||||||
|
// Save to persistent storage
|
||||||
|
await this.saveLinkedDirectories();
|
||||||
|
|
||||||
|
// Update video storage
|
||||||
|
await this.updateUnifiedVideoStorage();
|
||||||
|
|
||||||
|
this.showNotification(`Unlinked directory: ${directory.name}`, 'success');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unlinking directory:', error);
|
||||||
|
this.showNotification('Failed to unlink directory', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAllDirectories() {
|
||||||
|
if (!this.isElectron) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Refreshing all linked directories...');
|
||||||
|
this.allLinkedVideos = [];
|
||||||
|
|
||||||
|
for (const directory of this.externalVideoDirectories) {
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Rescanning: ${directory.name}`);
|
||||||
|
const videoFiles = await window.electronAPI.readVideoDirectoryRecursive(directory.path);
|
||||||
|
|
||||||
|
const linkedVideos = videoFiles.map(video => ({
|
||||||
|
id: `${directory.id}_${video.name}`,
|
||||||
|
name: video.name,
|
||||||
|
path: video.path,
|
||||||
|
url: `file:///${video.path.replace(/\\/g, '/')}`,
|
||||||
|
title: this.getVideoTitle(video.name),
|
||||||
|
size: video.size || 0,
|
||||||
|
directoryId: directory.id,
|
||||||
|
directoryName: directory.name,
|
||||||
|
isExternal: true,
|
||||||
|
relativePath: video.path.replace(directory.path, '').replace(/^[\\/]/, '')
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.allLinkedVideos.push(...linkedVideos);
|
||||||
|
|
||||||
|
// Update directory video count
|
||||||
|
directory.videoCount = videoFiles.length;
|
||||||
|
|
||||||
|
console.log(`✅ Found ${videoFiles.length} videos in ${directory.name}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not access directory ${directory.name}:`, error);
|
||||||
|
// Directory might be unavailable (external drive, network, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveLinkedDirectories();
|
||||||
|
await this.updateUnifiedVideoStorage();
|
||||||
|
|
||||||
|
const totalVideos = this.allLinkedVideos.length;
|
||||||
|
this.showNotification(`🔄 Refreshed ${this.externalVideoDirectories.length} directories, found ${totalVideos} videos`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveLinkedDirectories() {
|
||||||
|
const data = {
|
||||||
|
directories: this.externalVideoDirectories,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
this.dataManager.set('linkedVideoDirectories', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLinkedDirectories() {
|
||||||
|
if (!this.isElectron) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = this.dataManager.get('linkedVideoDirectories');
|
||||||
|
if (data && data.directories) {
|
||||||
|
this.externalVideoDirectories = data.directories;
|
||||||
|
console.log(`📁 Loaded ${this.externalVideoDirectories.length} linked directories`);
|
||||||
|
|
||||||
|
// Refresh all directories to get current video lists
|
||||||
|
await this.refreshAllDirectories();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading linked directories:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUnifiedVideoStorage() {
|
||||||
|
// Store all videos in a single unified list (no categories)
|
||||||
|
|
||||||
|
// If we have no linked directories but there are existing videos in storage,
|
||||||
|
// don't overwrite the existing data
|
||||||
|
if (this.externalVideoDirectories.length === 0 && this.allLinkedVideos.length === 0) {
|
||||||
|
// Check if there are existing videos in storage
|
||||||
|
try {
|
||||||
|
const existingData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
|
||||||
|
if (existingData.allVideos && existingData.allVideos.length > 0) {
|
||||||
|
console.log(`📹 Preserving existing unified video library: ${existingData.allVideos.length} videos`);
|
||||||
|
// Don't overwrite - preserve existing data
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error checking existing unified video library:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoData = {
|
||||||
|
allVideos: this.allLinkedVideos,
|
||||||
|
directories: this.externalVideoDirectories,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem('unifiedVideoLibrary', JSON.stringify(videoData));
|
||||||
|
console.log(`📹 Updated unified video library: ${this.allLinkedVideos.length} videos from ${this.externalVideoDirectories.length} directories`);
|
||||||
|
|
||||||
|
// Trigger video manager reload if it exists
|
||||||
|
if (window.videoPlayerManager) {
|
||||||
|
window.videoPlayerManager.loadVideoFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectoryName(directoryPath) {
|
||||||
|
// Extract just the folder name from the full path
|
||||||
|
return directoryPath.split(/[\\/]/).pop() || 'Unknown Directory';
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllVideos() {
|
||||||
|
return this.allLinkedVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectoriesInfo() {
|
||||||
|
return this.externalVideoDirectories.map(dir => ({
|
||||||
|
id: dir.id,
|
||||||
|
name: dir.name,
|
||||||
|
path: dir.path,
|
||||||
|
videoCount: dir.videoCount,
|
||||||
|
dateAdded: dir.dateAdded
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async scanDirectoryForImages(category = 'task') {
|
async scanDirectoryForImages(category = 'task') {
|
||||||
if (!this.isElectron) {
|
if (!this.isElectron) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -303,51 +584,6 @@ class DesktopFileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAllDirectories() {
|
|
||||||
if (!this.isElectron) {
|
|
||||||
this.showNotification('Directory scanning only available in desktop version', 'warning');
|
|
||||||
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,
|
|
||||||
videos: {
|
|
||||||
background: backgroundVideos,
|
|
||||||
tasks: taskVideos,
|
|
||||||
rewards: rewardVideos,
|
|
||||||
punishments: punishmentVideos
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update image storage
|
|
||||||
if (taskImages.length > 0 || consequenceImages.length > 0) {
|
|
||||||
await this.updateImageStorage([...taskImages, ...consequenceImages]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateImageStorage(images) {
|
async updateImageStorage(images) {
|
||||||
// Get existing images
|
// Get existing images
|
||||||
let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||||||
|
|
@ -666,19 +902,28 @@ class DesktopFileManager {
|
||||||
// Get existing videos from localStorage
|
// Get existing videos from localStorage
|
||||||
const existingVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
const existingVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
||||||
|
|
||||||
|
console.log('Updating video storage with', videoFiles.length, 'videos');
|
||||||
|
|
||||||
// Add new videos
|
// Add new videos
|
||||||
videoFiles.forEach(video => {
|
videoFiles.forEach(video => {
|
||||||
|
console.log(`Adding video to category: ${video.category}, name: ${video.name}`);
|
||||||
if (!existingVideos[video.category]) {
|
if (!existingVideos[video.category]) {
|
||||||
existingVideos[video.category] = [];
|
existingVideos[video.category] = [];
|
||||||
|
console.log(`Created new category: ${video.category}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if video already exists (prevent duplicates)
|
// Check if video already exists (prevent duplicates)
|
||||||
const exists = existingVideos[video.category].some(existing => existing.name === video.name);
|
const exists = existingVideos[video.category].some(existing => existing.name === video.name);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
existingVideos[video.category].push(video);
|
existingVideos[video.category].push(video);
|
||||||
|
console.log(`Added video: ${video.name} to ${video.category}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Video already exists: ${video.name} in ${video.category}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Final video storage:', existingVideos);
|
||||||
|
|
||||||
// Save back to localStorage
|
// Save back to localStorage
|
||||||
localStorage.setItem('videoFiles', JSON.stringify(existingVideos));
|
localStorage.setItem('videoFiles', JSON.stringify(existingVideos));
|
||||||
|
|
||||||
|
|
|
||||||
BIN
test-cinema.js
BIN
test-cinema.js
Binary file not shown.
|
|
@ -1,413 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Theme Mockup - Balanced Green</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Audiowide:wght@400&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
/* Balanced Green Color Variables */
|
|
||||||
:root {
|
|
||||||
/* Color system - Forest green accents with neutral backgrounds */
|
|
||||||
--color-primary: #228b22;
|
|
||||||
--color-secondary: #006400;
|
|
||||||
--color-accent: #32cd32;
|
|
||||||
--color-success: #28a745;
|
|
||||||
--color-warning: #ffc107;
|
|
||||||
--color-danger: #dc3545;
|
|
||||||
|
|
||||||
/* Text colors */
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #e0e0e0;
|
|
||||||
--text-tertiary: #b0b0b0;
|
|
||||||
--text-muted: #808080;
|
|
||||||
|
|
||||||
/* Background colors - Neutrals with subtle purple hints */
|
|
||||||
--bg-primary: #0a0a0a;
|
|
||||||
--bg-secondary: #1a1a1a;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--bg-card: rgba(42, 42, 42, 0.8);
|
|
||||||
--bg-modal: rgba(26, 26, 26, 0.95);
|
|
||||||
|
|
||||||
/* Border and shadow - Dark grays with forest green accents */
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-accent: rgba(34, 139, 34, 0.3);
|
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
--glow-primary: 0 0 20px rgba(34, 139, 34, 0.4);
|
|
||||||
|
|
||||||
/* Font system */
|
|
||||||
--font-xs: 0.75rem;
|
|
||||||
--font-sm: 0.875rem;
|
|
||||||
--font-base: 1rem;
|
|
||||||
--font-lg: 1.125rem;
|
|
||||||
--font-xl: 1.25rem;
|
|
||||||
--font-xxl: 1.5rem;
|
|
||||||
--font-xxxl: 2rem;
|
|
||||||
--font-xxxxl: 3rem;
|
|
||||||
|
|
||||||
/* Spacing system */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-base: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-xxl: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-primary) 100%);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
color: var(--text-primary);
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-xl) var(--space-base);
|
|
||||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, transparent 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-title {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
font-size: var(--font-xxxxl);
|
|
||||||
background: linear-gradient(45deg, var(--color-primary), var(--color-secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(34, 139, 34, 0.7);
|
|
||||||
margin-bottom: var(--space-base);
|
|
||||||
animation: titleGlow 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes titleGlow {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(34, 139, 34, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(34, 139, 34, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-tagline {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
color: var(--color-accent);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main container */
|
|
||||||
.main-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats section */
|
|
||||||
.stats-section {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: var(--space-base);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: var(--font-xxl);
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation buttons */
|
|
||||||
.nav-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: var(--space-lg);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--glow-primary);
|
|
||||||
background: linear-gradient(135deg, var(--color-secondary), var(--color-accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Progress section */
|
|
||||||
.progress-section {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-radius: 20px;
|
|
||||||
height: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: var(--space-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
|
|
||||||
height: 100%;
|
|
||||||
width: 65%;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 0 10px rgba(220, 20, 60, 0.5);
|
|
||||||
animation: progressPulse 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progressPulse {
|
|
||||||
from { box-shadow: 0 0 5px rgba(34, 139, 34, 0.3); }
|
|
||||||
to { box-shadow: 0 0 15px rgba(34, 139, 34, 0.7); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Recent activity */
|
|
||||||
.activity-section {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-item {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: var(--space-base);
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add forest green accent to important elements */
|
|
||||||
.stat-card:hover {
|
|
||||||
border-color: var(--border-accent);
|
|
||||||
box-shadow: 0 0 10px rgba(34, 139, 34, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-item:hover {
|
|
||||||
border-color: var(--border-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-text {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-time {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer info */
|
|
||||||
.footer-info {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-xl);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme showcase banner */
|
|
||||||
.theme-banner {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: var(--glow-primary);
|
|
||||||
animation: bannerPulse 3s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bannerPulse {
|
|
||||||
from { box-shadow: 0 0 15px rgba(34, 139, 34, 0.4); }
|
|
||||||
to { box-shadow: 0 0 25px rgba(34, 139, 34, 0.8); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-banner h2 {
|
|
||||||
font-size: var(--font-xxl);
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-banner p {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.game-title {
|
|
||||||
font-size: var(--font-xxxl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-section {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.game-title {
|
|
||||||
font-size: var(--font-xxl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1 class="game-title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="game-tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-container">
|
|
||||||
<!-- Theme showcase banner -->
|
|
||||||
<div class="theme-banner">
|
|
||||||
<h2>🌲 Balanced Forest Green Theme</h2>
|
|
||||||
<p>Natural forest green accents with neutral black/gray backgrounds - Organic, earthy feel</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats section -->
|
|
||||||
<div class="stats-section">
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">42</div>
|
|
||||||
<div class="stat-label">Sessions Completed</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">18h 32m</div>
|
|
||||||
<div class="stat-label">Total Training Time</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">Level 7</div>
|
|
||||||
<div class="stat-label">Current Rank</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">95%</div>
|
|
||||||
<div class="stat-label">Dedication Score</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation buttons -->
|
|
||||||
<div class="nav-section">
|
|
||||||
<button class="nav-button">🎯 Start Training Session</button>
|
|
||||||
<button class="nav-button">📚 Browse Content</button>
|
|
||||||
<button class="nav-button">🏆 View Achievements</button>
|
|
||||||
<button class="nav-button">⚙️ Settings & Preferences</button>
|
|
||||||
<button class="nav-button">📊 Progress Analytics</button>
|
|
||||||
<button class="nav-button">🎪 Interactive Tasks</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress section -->
|
|
||||||
<div class="progress-section">
|
|
||||||
<h3 class="progress-title">Current Progress</h3>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill"></div>
|
|
||||||
</div>
|
|
||||||
<p class="progress-text">65% through Intermediate Training Module</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recent activity -->
|
|
||||||
<div class="activity-section">
|
|
||||||
<h3 class="activity-title">Recent Activity</h3>
|
|
||||||
<div class="activity-item">
|
|
||||||
<span class="activity-text">Completed Focus Session: Advanced Techniques</span>
|
|
||||||
<span class="activity-time">2 hours ago</span>
|
|
||||||
</div>
|
|
||||||
<div class="activity-item">
|
|
||||||
<span class="activity-text">Achievement Unlocked: Dedication Master</span>
|
|
||||||
<span class="activity-time">Yesterday</span>
|
|
||||||
</div>
|
|
||||||
<div class="activity-item">
|
|
||||||
<span class="activity-text">Training Session: Endurance Building</span>
|
|
||||||
<span class="activity-time">2 days ago</span>
|
|
||||||
</div>
|
|
||||||
<div class="activity-item">
|
|
||||||
<span class="activity-text">Mirror Task: Self-Reflection Exercise</span>
|
|
||||||
<span class="activity-time">3 days ago</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-info">
|
|
||||||
<p>This is a mockup of Balanced Forest Green Theme applied to your game's main screen</p>
|
|
||||||
<p>Notice the neutral black/gray backgrounds with natural forest greens used only for accents, buttons, and highlights</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,509 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Option 4 - Color Variations & Game Palettes</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Audiowide&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-option {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 30px;
|
|
||||||
margin: 30px 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 30px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-section {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-number {
|
|
||||||
display: inline-block;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
font-size: 2.8rem;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-section h3 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-palette {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-swatch {
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-swatch .color-name {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-swatch .color-code {
|
|
||||||
font-size: 10px;
|
|
||||||
opacity: 0.8;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-info {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 3px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 1 - Original Pink/Orange */
|
|
||||||
.scheme-1 .title {
|
|
||||||
background: linear-gradient(45deg, #ff0080, #ff8000);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(255, 0, 128, 0.7);
|
|
||||||
animation: glow1 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-1 .tagline { color: #ff4d4d; }
|
|
||||||
.scheme-1 .usage-info { border-left-color: #ff0080; }
|
|
||||||
|
|
||||||
@keyframes glow1 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(255, 0, 128, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(255, 0, 128, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 2 - Electric Blue/Cyan */
|
|
||||||
.scheme-2 .title {
|
|
||||||
background: linear-gradient(45deg, #00bfff, #1e90ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(0, 191, 255, 0.7);
|
|
||||||
animation: glow2 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-2 .tagline { color: #4db8ff; }
|
|
||||||
.scheme-2 .usage-info { border-left-color: #00bfff; }
|
|
||||||
|
|
||||||
@keyframes glow2 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(0, 191, 255, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(0, 191, 255, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 3 - Purple/Violet */
|
|
||||||
.scheme-3 .title {
|
|
||||||
background: linear-gradient(45deg, #8a2be2, #da70d6);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(138, 43, 226, 0.7);
|
|
||||||
animation: glow3 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-3 .tagline { color: #ba55d3; }
|
|
||||||
.scheme-3 .usage-info { border-left-color: #8a2be2; }
|
|
||||||
|
|
||||||
@keyframes glow3 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(138, 43, 226, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(138, 43, 226, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 4 - Green/Lime */
|
|
||||||
.scheme-4 .title {
|
|
||||||
background: linear-gradient(45deg, #00ff00, #32cd32);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(0, 255, 0, 0.7);
|
|
||||||
animation: glow4 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-4 .tagline { color: #7fff00; }
|
|
||||||
.scheme-4 .usage-info { border-left-color: #00ff00; }
|
|
||||||
|
|
||||||
@keyframes glow4 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(0, 255, 0, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(0, 255, 0, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 5 - Red/Crimson */
|
|
||||||
.scheme-5 .title {
|
|
||||||
background: linear-gradient(45deg, #dc143c, #ff6347);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(220, 20, 60, 0.7);
|
|
||||||
animation: glow5 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-5 .tagline { color: #ff4757; }
|
|
||||||
.scheme-5 .usage-info { border-left-color: #dc143c; }
|
|
||||||
|
|
||||||
@keyframes glow5 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(220, 20, 60, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(220, 20, 60, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Scheme 6 - Gold/Amber */
|
|
||||||
.scheme-6 .title {
|
|
||||||
background: linear-gradient(45deg, #ffd700, #ffa500);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(255, 215, 0, 0.7);
|
|
||||||
animation: glow6 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.scheme-6 .tagline { color: #ffb347; }
|
|
||||||
.scheme-6 .usage-info { border-left-color: #ffd700; }
|
|
||||||
|
|
||||||
@keyframes glow6 {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(255, 215, 0, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.color-option {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 2rem !important;
|
|
||||||
}
|
|
||||||
.color-palette {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="intro">
|
|
||||||
<h1>Option 4: Gaming Aesthetic - Color Variations</h1>
|
|
||||||
<p>Audiowide font with different color schemes and complete game palettes</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 1 - Original Pink/Orange -->
|
|
||||||
<div class="color-option scheme-1">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">A</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>🌈 Pink/Orange Gaming Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #ff0080; color: white;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#ff0080</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ff8000; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#ff8000</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ff4d4d; color: white;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#ff4d4d</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #2d2d2d; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#2d2d2d</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1a1a1a; color: white;">
|
|
||||||
<div class="color-name">Darker BG</div>
|
|
||||||
<div class="color-code">#1a1a1a</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ff0080; opacity: 0.1; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(255,0,128,0.1)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> High-energy gaming vibe with pink primary for buttons/highlights, orange for progress bars/completion states, dark backgrounds for contrast. Perfect for an intense, exciting gaming experience.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 2 - Electric Blue/Cyan -->
|
|
||||||
<div class="color-option scheme-2">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">B</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>💙 Electric Blue/Cyan Tech Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #00bfff; color: white;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#00bfff</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1e90ff; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#1e90ff</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #4db8ff; color: white;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#4db8ff</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #0f2027; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#0f2027</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #203a43; color: white;">
|
|
||||||
<div class="color-name">Mid BG</div>
|
|
||||||
<div class="color-code">#203a43</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #00ffff; opacity: 0.15; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(0,255,255,0.15)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> Professional tech aesthetic with blue primary for navigation/buttons, cyan for active states/progress, dark blue-grey backgrounds. Creates a sophisticated, high-tech training environment feel.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 3 - Purple/Violet -->
|
|
||||||
<div class="color-option scheme-3">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">C</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>💜 Purple/Violet Luxe Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #8a2be2; color: white;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#8a2be2</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #da70d6; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#da70d6</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ba55d3; color: white;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#ba55d3</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1a0d26; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#1a0d26</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #2d1b3d; color: white;">
|
|
||||||
<div class="color-name">Mid BG</div>
|
|
||||||
<div class="color-code">#2d1b3d</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #8a2be2; opacity: 0.12; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(138,43,226,0.12)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> Luxurious, premium feel with purple primary for main actions, orchid for highlights/hover states, deep purple backgrounds. Creates an exclusive, high-end academy atmosphere.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 4 - Green/Lime -->
|
|
||||||
<div class="color-option scheme-4">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">D</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>💚 Green/Lime Matrix Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #00ff00; color: black;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#00ff00</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #32cd32; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#32cd32</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #7fff00; color: black;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#7fff00</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #0d1a0d; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#0d1a0d</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1b2d1b; color: white;">
|
|
||||||
<div class="color-name">Mid BG</div>
|
|
||||||
<div class="color-code">#1b2d1b</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #00ff00; opacity: 0.08; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(0,255,0,0.08)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> Matrix/hacker aesthetic with bright green primary for success states/progress, lime for active elements, very dark green backgrounds. Perfect for a digital training simulation vibe.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 5 - Red/Crimson -->
|
|
||||||
<div class="color-option scheme-5">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">E</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>❤️ Red/Crimson Power Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #dc143c; color: white;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#dc143c</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ff6347; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#ff6347</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ff4757; color: white;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#ff4757</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1a0a0a; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#1a0a0a</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #2d1515; color: white;">
|
|
||||||
<div class="color-name">Mid BG</div>
|
|
||||||
<div class="color-code">#2d1515</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #dc143c; opacity: 0.1; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(220,20,60,0.1)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> Bold, intense atmosphere with crimson primary for warnings/important actions, coral for completion states, dark red backgrounds. Creates a powerful, commanding training environment.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Scheme 6 - Gold/Amber -->
|
|
||||||
<div class="color-option scheme-6">
|
|
||||||
<div class="title-section">
|
|
||||||
<div class="option-number">F</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
</div>
|
|
||||||
<div class="palette-section">
|
|
||||||
<h3>🏆 Gold/Amber Elite Palette</h3>
|
|
||||||
<div class="color-palette">
|
|
||||||
<div class="color-swatch" style="background: #ffd700; color: black;">
|
|
||||||
<div class="color-name">Primary</div>
|
|
||||||
<div class="color-code">#ffd700</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ffa500; color: white;">
|
|
||||||
<div class="color-name">Secondary</div>
|
|
||||||
<div class="color-code">#ffa500</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ffb347; color: black;">
|
|
||||||
<div class="color-name">Accent</div>
|
|
||||||
<div class="color-code">#ffb347</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #1a1a0d; color: white;">
|
|
||||||
<div class="color-name">Dark BG</div>
|
|
||||||
<div class="color-code">#1a1a0d</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #2d2d1b; color: white;">
|
|
||||||
<div class="color-name">Mid BG</div>
|
|
||||||
<div class="color-code">#2d2d1b</div>
|
|
||||||
</div>
|
|
||||||
<div class="color-swatch" style="background: #ffd700; opacity: 0.08; color: white;">
|
|
||||||
<div class="color-name">Glow</div>
|
|
||||||
<div class="color-code">rgba(255,215,0,0.08)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="usage-info">
|
|
||||||
<strong>Game Implementation:</strong> Premium, achievement-focused design with gold primary for rewards/completion, orange for progress indicators, warm dark backgrounds. Perfect for highlighting accomplishments and elite status.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: rgba(0, 0, 0, 0.6); border: 2px solid #444; border-radius: 15px; padding: 30px; margin: 40px 0; text-align: center;">
|
|
||||||
<h2 style="margin-bottom: 15px; font-size: 24px;">🎨 Choose Your Academy's Identity!</h2>
|
|
||||||
<p style="font-size: 16px; line-height: 1.6; color: #ccc;">Each color scheme creates a completely different atmosphere for your training academy. Consider what mood and personality best fits your vision:</p>
|
|
||||||
<ul style="text-align: left; max-width: 600px; margin: 20px auto; color: #bbb;">
|
|
||||||
<li><strong>A - Pink/Orange:</strong> High-energy gaming excitement</li>
|
|
||||||
<li><strong>B - Blue/Cyan:</strong> Professional tech sophistication</li>
|
|
||||||
<li><strong>C - Purple/Violet:</strong> Luxurious premium experience</li>
|
|
||||||
<li><strong>D - Green/Lime:</strong> Matrix-style digital simulation</li>
|
|
||||||
<li><strong>E - Red/Crimson:</strong> Bold commanding intensity</li>
|
|
||||||
<li><strong>F - Gold/Amber:</strong> Elite achievement-focused</li>
|
|
||||||
</ul>
|
|
||||||
<p style="margin-top: 20px;"><strong>Just tell me which letter you prefer!</strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Gooner Training Academy - Title Design Options</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;600;700;800&family=Rajdhani:wght@300;400;500;600;700&family=Russo+One&family=Bebas+Neue&family=Audiowide&family=Saira+Condensed:wght@300;400;500;600;700&family=Titillium+Web:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.design-option {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 30px;
|
|
||||||
margin: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-number {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 15px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 1 - Futuristic Tech */
|
|
||||||
.option-1 .title {
|
|
||||||
font-family: 'Orbitron', monospace;
|
|
||||||
font-size: 3.5rem;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(45deg, #00ffff, #0080ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
|
|
||||||
letter-spacing: 3px;
|
|
||||||
}
|
|
||||||
.option-1 .tagline {
|
|
||||||
font-family: 'Orbitron', monospace;
|
|
||||||
color: #00bfff;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 2 - Military Academy */
|
|
||||||
.option-2 .title {
|
|
||||||
font-family: 'Bebas Neue', cursive;
|
|
||||||
font-size: 4rem;
|
|
||||||
color: #ff6b35;
|
|
||||||
text-shadow:
|
|
||||||
2px 2px 0px #000,
|
|
||||||
4px 4px 0px #333,
|
|
||||||
6px 6px 10px rgba(0,0,0,0.8);
|
|
||||||
letter-spacing: 4px;
|
|
||||||
transform: perspective(500px) rotateX(10deg);
|
|
||||||
}
|
|
||||||
.option-2 .tagline {
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
color: #ffa500;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 3 - Sleek Modern */
|
|
||||||
.option-3 .title {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
font-size: 3.8rem;
|
|
||||||
font-weight: 800;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
.option-3 .tagline {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
color: #8e94f2;
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 4 - Gaming Aesthetic */
|
|
||||||
.option-4 .title {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
font-size: 3.2rem;
|
|
||||||
background: linear-gradient(45deg, #ff0080, #ff8000);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 30px rgba(255, 0, 128, 0.7);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
animation: glow 2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.option-4 .tagline {
|
|
||||||
font-family: 'Audiowide', cursive;
|
|
||||||
color: #ff4d4d;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glow {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(255, 0, 128, 0.5)); }
|
|
||||||
to { filter: drop-shadow(0 0 15px rgba(255, 0, 128, 0.9)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 5 - Professional Bold */
|
|
||||||
.option-5 .title {
|
|
||||||
font-family: 'Russo One', sans-serif;
|
|
||||||
font-size: 3.6rem;
|
|
||||||
color: #2ecc71;
|
|
||||||
text-shadow:
|
|
||||||
1px 1px 0px #000,
|
|
||||||
2px 2px 0px #1a5c3a,
|
|
||||||
3px 3px 5px rgba(0,0,0,0.5);
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
.option-5 .tagline {
|
|
||||||
font-family: 'Titillium Web', sans-serif;
|
|
||||||
color: #27ae60;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 6 - Elegant Minimalist */
|
|
||||||
.option-6 .title {
|
|
||||||
font-family: 'Saira Condensed', sans-serif;
|
|
||||||
font-size: 4.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ecf0f1;
|
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
.option-6 .tagline {
|
|
||||||
font-family: 'Saira Condensed', sans-serif;
|
|
||||||
color: #bdc3c7;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 7 - Neon Cyberpunk */
|
|
||||||
.option-7 .title {
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
font-size: 3.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ff073a;
|
|
||||||
text-shadow:
|
|
||||||
0 0 5px #ff073a,
|
|
||||||
0 0 10px #ff073a,
|
|
||||||
0 0 15px #ff073a,
|
|
||||||
0 0 20px #ff073a;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
animation: neonFlicker 3s infinite;
|
|
||||||
}
|
|
||||||
.option-7 .tagline {
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
color: #ff1744;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes neonFlicker {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.8; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 8 - Gold Luxury */
|
|
||||||
.option-8 .title {
|
|
||||||
font-family: 'Titillium Web', sans-serif;
|
|
||||||
font-size: 3.7rem;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
.option-8 .tagline {
|
|
||||||
font-family: 'Titillium Web', sans-serif;
|
|
||||||
color: #f39c12;
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 9 - Purple Power */
|
|
||||||
.option-9 .title {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
font-size: 3.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
background: linear-gradient(135deg, #8e2de2, #4a00e0);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 20px rgba(142, 45, 226, 0.6);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
.option-9 .tagline {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
color: #9d4edd;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Design Option 10 - Fire Theme */
|
|
||||||
.option-10 .title {
|
|
||||||
font-family: 'Bebas Neue', cursive;
|
|
||||||
font-size: 4rem;
|
|
||||||
background: linear-gradient(45deg, #ff4500, #ff6347, #ff0000);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 0 25px rgba(255, 69, 0, 0.8);
|
|
||||||
letter-spacing: 3px;
|
|
||||||
animation: fireGlow 2.5s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.option-10 .tagline {
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
color: #ff6b47;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fireGlow {
|
|
||||||
from { filter: drop-shadow(0 0 5px rgba(255, 69, 0, 0.6)); }
|
|
||||||
to { filter: drop-shadow(0 0 20px rgba(255, 69, 0, 1)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin-top: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-section {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border: 2px solid #444;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 30px;
|
|
||||||
margin: 40px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-section h2 {
|
|
||||||
color: #fff;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-section p {
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem !important;
|
|
||||||
}
|
|
||||||
.tagline {
|
|
||||||
font-size: 14px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="intro">
|
|
||||||
<h1>Gooner Training Academy - Title Design Options</h1>
|
|
||||||
<p>Choose your favorite design for the main header!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-1">
|
|
||||||
<div class="option-number">1</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Futuristic Tech - Orbitron font with cyan gradients and glow effects</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-2">
|
|
||||||
<div class="option-number">2</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Military Academy - Bebas Neue with orange theme and 3D perspective</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-3">
|
|
||||||
<div class="option-number">3</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Sleek Modern - Exo 2 font with purple-blue gradients</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-4">
|
|
||||||
<div class="option-number">4</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Gaming Aesthetic - Audiowide font with pink-orange gradients and glow animation</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-5">
|
|
||||||
<div class="option-number">5</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Professional Bold - Russo One font with green theme and layered shadows</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-6">
|
|
||||||
<div class="option-number">6</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Elegant Minimalist - Saira Condensed with clean white text and subtle shadows</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-7">
|
|
||||||
<div class="option-number">7</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Neon Cyberpunk - Rajdhani font with red neon glow and flicker animation</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-8">
|
|
||||||
<div class="option-number">8</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Gold Luxury - Titillium Web with gold gradients for premium feel</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-9">
|
|
||||||
<div class="option-number">9</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Purple Power - Exo 2 font with purple gradients and ethereal glow</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="design-option option-10">
|
|
||||||
<div class="option-number">10</div>
|
|
||||||
<h1 class="title">GOONER TRAINING ACADEMY</h1>
|
|
||||||
<p class="tagline">Master Your Dedication</p>
|
|
||||||
<div class="description">Fire Theme - Bebas Neue with red-orange fire gradients and pulse animation</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vote-section">
|
|
||||||
<h2>🎯 Your Choice Matters!</h2>
|
|
||||||
<p>Each design option has its own personality and vibe. Consider which one best represents the sophisticated training platform you've built with interactive scenarios, TTS narration, and advanced focus challenges.</p>
|
|
||||||
<p><strong>Just tell me which number(s) you prefer!</strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
535
tts-test.html
535
tts-test.html
|
|
@ -1,535 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Text-to-Speech Testing</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 50px auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-section {
|
|
||||||
background: #34495e;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid #673ab7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin: 15px 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: linear-gradient(135deg, #673ab7, #5e35b1);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: linear-gradient(135deg, #5e35b1, #512da8);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background: #666;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
select, input[type="range"] {
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-text {
|
|
||||||
background: #2c3e50;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #673ab7;
|
|
||||||
font-style: italic;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-info {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #bdc3c7;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quality-indicator {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quality-high { background: #27ae60; }
|
|
||||||
.quality-medium { background: #f39c12; }
|
|
||||||
.quality-low { background: #e74c3c; }
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-right: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>🎙️ Electron Female Voice TTS Testing Lab</h1>
|
|
||||||
<p>Test female voices available in your Electron app. Shows system voices and Chromium built-ins that will work in your desktop application.</p>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>Electron Female Voice Selection & Settings</h2>
|
|
||||||
<p><strong>🚺 Female voices only</strong> - Filtered to show female voices available in Electron applications.</p>
|
|
||||||
<p style="font-size: 14px; color: #95a5a6;">
|
|
||||||
<strong>⚡ Electron Environment:</strong> Shows system voices (Windows SAPI, macOS System) and Chromium built-ins. These are the actual voices your users will hear.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<label>
|
|
||||||
Voice:
|
|
||||||
<select id="voice-select" style="min-width: 300px;">
|
|
||||||
<option>Loading voices...</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="voice-info" id="voice-info">Select a voice to see details...</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<label>
|
|
||||||
Speed:
|
|
||||||
<input type="range" id="rate-slider" min="0.3" max="2" step="0.1" value="0.8">
|
|
||||||
<span id="rate-display">0.8x</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Pitch:
|
|
||||||
<input type="range" id="pitch-slider" min="0.5" max="2" step="0.1" value="1">
|
|
||||||
<span id="pitch-display">1.0</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Volume:
|
|
||||||
<input type="range" id="volume-slider" min="0" max="1" step="0.1" value="0.7">
|
|
||||||
<span id="volume-display">70%</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>Sample Scenario Content</h2>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button onclick="speakText(sampleTexts.intro)">🎭 Scenario Introduction</button>
|
|
||||||
<button onclick="speakText(sampleTexts.instruction)">📋 Task Instruction</button>
|
|
||||||
<button onclick="speakText(sampleTexts.feedback)">💬 Feedback Message</button>
|
|
||||||
<button onclick="speakText(sampleTexts.ending)">🏁 Scenario Ending</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Custom Text Testing</h3>
|
|
||||||
<textarea id="custom-text" rows="4" style="width: 100%; padding: 10px; border-radius: 5px; background: #34495e; color: white; border: 1px solid #673ab7;">
|
|
||||||
Enter your own text here to test how it sounds with TTS...
|
|
||||||
</textarea>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button onclick="speakCustomText()">🗣️ Speak Custom Text</button>
|
|
||||||
<button onclick="stopSpeaking()">⏹️ Stop</button>
|
|
||||||
<button onclick="pauseSpeaking()">⏸️ Pause</button>
|
|
||||||
<button onclick="resumeSpeaking()">▶️ Resume</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>Sample Texts Being Tested</h2>
|
|
||||||
|
|
||||||
<div class="scenario-text">
|
|
||||||
<h4>Scenario Introduction:</h4>
|
|
||||||
<div id="intro-text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scenario-text">
|
|
||||||
<h4>Task Instruction:</h4>
|
|
||||||
<div id="instruction-text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scenario-text">
|
|
||||||
<h4>Feedback Message:</h4>
|
|
||||||
<div id="feedback-text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scenario-text">
|
|
||||||
<h4>Scenario Ending:</h4>
|
|
||||||
<div id="ending-text"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>Electron Female Voices by Platform</h2>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 15px 0;">
|
|
||||||
<div>
|
|
||||||
<h4>🖥️ Windows in Electron:</h4>
|
|
||||||
<ul style="font-size: 14px;">
|
|
||||||
<li><strong>Microsoft Zira Desktop</strong> ⭐ (Default)</li>
|
|
||||||
<li><strong>Microsoft Hazel Desktop</strong></li>
|
|
||||||
<li>Microsoft Eva Desktop</li>
|
|
||||||
<li>Microsoft Aria (if updated)</li>
|
|
||||||
<li>Microsoft Jenny (if updated)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4>🍎 macOS in Electron:</h4>
|
|
||||||
<ul style="font-size: 14px;">
|
|
||||||
<li><strong>Samantha</strong> ⭐ (Default female)</li>
|
|
||||||
<li><strong>Alex</strong> (Can be female-sounding)</li>
|
|
||||||
<li>Victoria</li>
|
|
||||||
<li>Karen</li>
|
|
||||||
<li>Fiona</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4>⚡ Electron Notes:</h4>
|
|
||||||
<ul style="font-size: 14px;">
|
|
||||||
<li><strong>System voices only</strong> - No Google/Amazon voices</li>
|
|
||||||
<li><strong>Quality varies</strong> - Depends on OS updates</li>
|
|
||||||
<li><strong>Local processing</strong> - No internet required</li>
|
|
||||||
<li><strong>Consistent</strong> - Same voice for all users on same OS</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #3498db; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
|
||||||
<h4 style="margin: 0 0 10px 0;">💡 Electron TTS Best Practices:</h4>
|
|
||||||
<ul style="margin: 5px 0; font-size: 14px;">
|
|
||||||
<li><strong>Windows:</strong> Zira is most reliable, Hazel for variety</li>
|
|
||||||
<li><strong>macOS:</strong> Samantha is the go-to female voice</li>
|
|
||||||
<li><strong>Fallback:</strong> Always provide text backup for accessibility</li>
|
|
||||||
<li><strong>Testing:</strong> Test on actual target OS, not just browser</li>
|
|
||||||
<li><strong>Chrome users:</strong> Online voices often sound better than local ones</li>
|
|
||||||
<li><strong>Speed matters:</strong> 0.7-0.8x often sounds more natural and intimate</li>
|
|
||||||
<li><strong>Pitch adjustment:</strong> Slightly lower pitch (0.9) can sound more mature</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// TTS Manager Class
|
|
||||||
class TTSTestManager {
|
|
||||||
constructor() {
|
|
||||||
this.synth = window.speechSynthesis;
|
|
||||||
this.currentUtterance = null;
|
|
||||||
this.selectedVoice = null;
|
|
||||||
this.settings = {
|
|
||||||
rate: 0.8,
|
|
||||||
pitch: 1.0,
|
|
||||||
volume: 0.7
|
|
||||||
};
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.loadVoices();
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.displaySampleTexts();
|
|
||||||
|
|
||||||
// Voices might load asynchronously
|
|
||||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
|
||||||
speechSynthesis.onvoiceschanged = () => this.loadVoices();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadVoices() {
|
|
||||||
const voices = this.synth.getVoices();
|
|
||||||
const select = document.getElementById('voice-select');
|
|
||||||
|
|
||||||
// Clear existing options
|
|
||||||
select.innerHTML = '';
|
|
||||||
|
|
||||||
if (voices.length === 0) {
|
|
||||||
select.innerHTML = '<option>No voices available</option>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter for English female voices only
|
|
||||||
const femaleVoices = voices.filter(voice => {
|
|
||||||
return voice.lang.startsWith('en') && this.isFemaleVoice(voice);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (femaleVoices.length === 0) {
|
|
||||||
select.innerHTML = '<option>No female voices found</option>';
|
|
||||||
console.log('All available voices:', voices.map(v => ({ name: v.name, lang: v.lang })));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort female voices by quality
|
|
||||||
const sortedVoices = femaleVoices.sort((a, b) => {
|
|
||||||
const aQuality = this.getVoiceQuality(a);
|
|
||||||
const bQuality = this.getVoiceQuality(b);
|
|
||||||
return bQuality - aQuality;
|
|
||||||
});
|
|
||||||
|
|
||||||
sortedVoices.forEach((voice, index) => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = index;
|
|
||||||
option.textContent = voice.name;
|
|
||||||
|
|
||||||
// Add quality indicator
|
|
||||||
const quality = this.getVoiceQuality(voice);
|
|
||||||
const qualityText = quality === 3 ? '⭐ High' : quality === 2 ? '✓ Medium' : '○ Basic';
|
|
||||||
option.textContent += ` (${qualityText})`;
|
|
||||||
|
|
||||||
select.appendChild(option);
|
|
||||||
|
|
||||||
// Select first high-quality voice as default
|
|
||||||
if (!this.selectedVoice && quality >= 2) {
|
|
||||||
this.selectedVoice = voice;
|
|
||||||
option.selected = true;
|
|
||||||
this.updateVoiceInfo(voice);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback to first female voice if no high-quality voice found
|
|
||||||
if (!this.selectedVoice && femaleVoices.length > 0) {
|
|
||||||
this.selectedVoice = femaleVoices[0];
|
|
||||||
this.updateVoiceInfo(femaleVoices[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log all detected female voices for debugging
|
|
||||||
console.log('🎙️ Female voices found:', femaleVoices.map(v => ({
|
|
||||||
name: v.name,
|
|
||||||
quality: this.getVoiceQuality(v),
|
|
||||||
local: v.localService
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
isFemaleVoice(voice) {
|
|
||||||
const name = voice.name.toLowerCase();
|
|
||||||
|
|
||||||
// Electron-specific female voices (system voices + Chromium built-ins)
|
|
||||||
const electronFemaleVoices = [
|
|
||||||
// Windows SAPI voices (available in Electron on Windows)
|
|
||||||
'zira', 'hazel', 'eva', 'aria', 'jenny',
|
|
||||||
|
|
||||||
// macOS system voices (available in Electron on Mac)
|
|
||||||
'samantha', 'alex', 'victoria', 'karen', 'fiona', 'moira', 'tessa',
|
|
||||||
'amelie', 'kyoko', 'mei-jia', 'sin-ji', 'ting-ting', 'yu-shu',
|
|
||||||
|
|
||||||
// Microsoft voices (may be available through system)
|
|
||||||
'cortana', 'eva', 'hedda', 'helle', 'herena',
|
|
||||||
|
|
||||||
// Chromium built-in voices (limited but cross-platform)
|
|
||||||
'female', 'woman',
|
|
||||||
|
|
||||||
// Common female names that appear in system voices
|
|
||||||
'anna', 'emma', 'mary', 'susan', 'kate', 'sara', 'laura', 'helena'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Female voice indicators
|
|
||||||
const femaleIndicators = ['female', 'woman', 'girl', 'lady'];
|
|
||||||
|
|
||||||
// Check for Electron-compatible female voices
|
|
||||||
const isElectronFemale = electronFemaleVoices.some(femaleName => name.includes(femaleName));
|
|
||||||
const hasIndicator = femaleIndicators.some(indicator => name.includes(indicator));
|
|
||||||
|
|
||||||
return isElectronFemale || hasIndicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVoiceQuality(voice) {
|
|
||||||
const name = voice.name.toLowerCase();
|
|
||||||
|
|
||||||
// Highest quality: System voices (best in Electron)
|
|
||||||
if (name.includes('zira') || name.includes('samantha') || name.includes('aria') ||
|
|
||||||
name.includes('eva') || name.includes('hazel')) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// High quality: Good system voices
|
|
||||||
if (name.includes('alex') || name.includes('victoria') || name.includes('karen') ||
|
|
||||||
name.includes('jenny') || name.includes('cortana')) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Medium quality: Standard system voices
|
|
||||||
if (name.includes('fiona') || name.includes('moira') || name.includes('tessa') ||
|
|
||||||
voice.localService === true) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic quality: Fallback voices
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVoiceInfo(voice) {
|
|
||||||
const info = document.getElementById('voice-info');
|
|
||||||
const quality = this.getVoiceQuality(voice);
|
|
||||||
const qualityClass = quality === 3 ? 'quality-high' : quality === 2 ? 'quality-medium' : 'quality-low';
|
|
||||||
const qualityText = quality === 3 ? 'High Quality' : quality === 2 ? 'Medium Quality' : 'Basic Quality';
|
|
||||||
|
|
||||||
info.innerHTML = `
|
|
||||||
<strong>${voice.name}</strong> - ${voice.lang}
|
|
||||||
<span class="quality-indicator ${qualityClass}">${qualityText}</span><br>
|
|
||||||
Local: ${voice.localService ? 'Yes' : 'No'} |
|
|
||||||
Default: ${voice.default ? 'Yes' : 'No'}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Voice selection
|
|
||||||
document.getElementById('voice-select').addEventListener('change', (e) => {
|
|
||||||
const voices = this.synth.getVoices().filter(v => v.lang.startsWith('en') && this.isFemaleVoice(v));
|
|
||||||
this.selectedVoice = voices[e.target.value];
|
|
||||||
this.updateVoiceInfo(this.selectedVoice);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rate slider
|
|
||||||
const rateSlider = document.getElementById('rate-slider');
|
|
||||||
const rateDisplay = document.getElementById('rate-display');
|
|
||||||
rateSlider.addEventListener('input', (e) => {
|
|
||||||
this.settings.rate = parseFloat(e.target.value);
|
|
||||||
rateDisplay.textContent = `${this.settings.rate}x`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pitch slider
|
|
||||||
const pitchSlider = document.getElementById('pitch-slider');
|
|
||||||
const pitchDisplay = document.getElementById('pitch-display');
|
|
||||||
pitchSlider.addEventListener('input', (e) => {
|
|
||||||
this.settings.pitch = parseFloat(e.target.value);
|
|
||||||
pitchDisplay.textContent = this.settings.pitch.toFixed(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Volume slider
|
|
||||||
const volumeSlider = document.getElementById('volume-slider');
|
|
||||||
const volumeDisplay = document.getElementById('volume-display');
|
|
||||||
volumeSlider.addEventListener('input', (e) => {
|
|
||||||
this.settings.volume = parseFloat(e.target.value);
|
|
||||||
volumeDisplay.textContent = `${Math.round(this.settings.volume * 100)}%`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
speak(text) {
|
|
||||||
// Stop any current speech
|
|
||||||
this.stop();
|
|
||||||
|
|
||||||
if (!text.trim()) return;
|
|
||||||
|
|
||||||
// Create utterance
|
|
||||||
this.currentUtterance = new SpeechSynthesisUtterance(text);
|
|
||||||
|
|
||||||
// Apply settings
|
|
||||||
if (this.selectedVoice) {
|
|
||||||
this.currentUtterance.voice = this.selectedVoice;
|
|
||||||
}
|
|
||||||
this.currentUtterance.rate = this.settings.rate;
|
|
||||||
this.currentUtterance.pitch = this.settings.pitch;
|
|
||||||
this.currentUtterance.volume = this.settings.volume;
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
this.currentUtterance.onstart = () => {
|
|
||||||
console.log('🎙️ Speech started');
|
|
||||||
this.updateButtonStates(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.currentUtterance.onend = () => {
|
|
||||||
console.log('🎙️ Speech ended');
|
|
||||||
this.updateButtonStates(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.currentUtterance.onerror = (event) => {
|
|
||||||
console.error('🎙️ Speech error:', event.error);
|
|
||||||
this.updateButtonStates(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start speaking
|
|
||||||
this.synth.speak(this.currentUtterance);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.synth.cancel();
|
|
||||||
this.updateButtonStates(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this.synth.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
resume() {
|
|
||||||
this.synth.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateButtonStates(speaking) {
|
|
||||||
const buttons = document.querySelectorAll('button');
|
|
||||||
// You could disable/enable buttons based on speaking state
|
|
||||||
// For now, just log the state
|
|
||||||
console.log('Speaking state:', speaking);
|
|
||||||
}
|
|
||||||
|
|
||||||
displaySampleTexts() {
|
|
||||||
document.getElementById('intro-text').textContent = sampleTexts.intro;
|
|
||||||
document.getElementById('instruction-text').textContent = sampleTexts.instruction;
|
|
||||||
document.getElementById('feedback-text').textContent = sampleTexts.feedback;
|
|
||||||
document.getElementById('ending-text').textContent = sampleTexts.ending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample texts for testing (similar to your game content)
|
|
||||||
const sampleTexts = {
|
|
||||||
intro: "Welcome to your training session. Today you will learn the importance of focus and concentration. Your excitement will be carefully managed as you progress through each challenge. Remember, obedience and attention to detail are essential for success.",
|
|
||||||
|
|
||||||
instruction: "Position yourself comfortably and maintain perfect stillness. Focus your attention on the screen while maintaining your breathing. You must hold this position for exactly sixty seconds without any movement. Your discipline will be tested.",
|
|
||||||
|
|
||||||
feedback: "Excellent work. Your focus is improving with each session. You maintained concentration for the full duration and demonstrated proper obedience. Your excitement level is now at moderate levels, exactly where it should be.",
|
|
||||||
|
|
||||||
ending: "Training session completed successfully. Final state: Excitement High, Focus Excellent. You have demonstrated exceptional concentration and discipline throughout this session. Your progress has been documented and you may now proceed to the next level."
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize TTS manager
|
|
||||||
let ttsManager;
|
|
||||||
|
|
||||||
// Wait for page load
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
ttsManager = new TTSTestManager();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Global functions for buttons
|
|
||||||
function speakText(text) {
|
|
||||||
ttsManager.speak(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function speakCustomText() {
|
|
||||||
const text = document.getElementById('custom-text').value;
|
|
||||||
ttsManager.speak(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSpeaking() {
|
|
||||||
ttsManager.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pauseSpeaking() {
|
|
||||||
ttsManager.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resumeSpeaking() {
|
|
||||||
ttsManager.resume();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Video Player Test</title>
|
|
||||||
<link rel="stylesheet" href="src/styles/base-video-player.css">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #fff;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.test-container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.test-section {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.video-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 400px;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.test-log {
|
|
||||||
background: #222;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
height: 200px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="test-container">
|
|
||||||
<h1>🎬 Video Player Integration Test</h1>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>BaseVideoPlayer Test</h2>
|
|
||||||
<div id="base-video-container" class="video-container">
|
|
||||||
<video id="base-video-element" style="width: 100%; height: 100%;">
|
|
||||||
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<button onclick="testBasePlayer()">Test BaseVideoPlayer</button>
|
|
||||||
<button onclick="clearLog()">Clear Log</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>FocusVideoPlayer Test</h2>
|
|
||||||
<div id="focus-video-container" class="video-container" style="display: none;">
|
|
||||||
<div class="video-player-container">
|
|
||||||
<video id="focus-video-player" style="width: 100%; height: 90%;">
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div class="video-controls" style="display: flex; align-items: center; gap: 15px; margin-top: 10px;">
|
|
||||||
<button class="play-pause-btn" style="background: none; border: none; color: #ff6b9d; font-size: 18px; cursor: pointer;">⏸️</button>
|
|
||||||
<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="flex: 1; accent-color: #ff6b9d;">
|
|
||||||
<span id="focus-volume-display" style="color: #bbb; font-size: 14px; min-width: 40px;">50%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="video-info" id="video-info" style="color: #bbb; font-size: 12px; margin-top: 5px; text-align: center;">
|
|
||||||
Ready to play videos
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<button onclick="testFocusPlayer()">Test FocusVideoPlayer</button>
|
|
||||||
<button onclick="stopFocusPlayer()">Stop Focus Player</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-section">
|
|
||||||
<h2>Test Log</h2>
|
|
||||||
<div id="test-log" class="test-log"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Load our video player scripts -->
|
|
||||||
<script src="src/features/media/baseVideoPlayer.js"></script>
|
|
||||||
<script src="src/features/media/focusVideoPlayer.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let basePlayer = null;
|
|
||||||
let focusPlayer = null;
|
|
||||||
|
|
||||||
// Mock video manager for testing
|
|
||||||
window.videoPlayerManager = {
|
|
||||||
videoLibrary: {
|
|
||||||
task: [
|
|
||||||
{ name: "Test Video 1", path: "https://www.w3schools.com/html/mov_bbb.mp4" },
|
|
||||||
{ name: "Test Video 2", path: "https://www.w3schools.com/html/movie.mp4" }
|
|
||||||
],
|
|
||||||
background: [
|
|
||||||
{ name: "Background Test", path: "https://www.w3schools.com/html/mov_bbb.mp4" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
getVideosByCategory: function(category) {
|
|
||||||
return this.videoLibrary[category] || [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function log(message) {
|
|
||||||
const logElement = document.getElementById('test-log');
|
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
|
||||||
logElement.textContent += `[${timestamp}] ${message}\n`;
|
|
||||||
logElement.scrollTop = logElement.scrollHeight;
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLog() {
|
|
||||||
document.getElementById('test-log').textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function testBasePlayer() {
|
|
||||||
try {
|
|
||||||
log('🎬 Testing BaseVideoPlayer...');
|
|
||||||
|
|
||||||
if (!window.BaseVideoPlayer) {
|
|
||||||
log('❌ BaseVideoPlayer class not found!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
basePlayer = new BaseVideoPlayer('#base-video-container', {
|
|
||||||
showControls: true,
|
|
||||||
autoHide: false,
|
|
||||||
showProgress: true,
|
|
||||||
showVolume: true,
|
|
||||||
showFullscreen: true,
|
|
||||||
keyboardShortcuts: true
|
|
||||||
});
|
|
||||||
|
|
||||||
log('✅ BaseVideoPlayer instance created successfully');
|
|
||||||
|
|
||||||
// Try to load a test video
|
|
||||||
basePlayer.loadVideo('https://www.w3schools.com/html/mov_bbb.mp4', false);
|
|
||||||
log('📺 Test video loaded');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ BaseVideoPlayer test failed: ${error.message}`);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function testFocusPlayer() {
|
|
||||||
try {
|
|
||||||
log('🧘 Testing FocusVideoPlayer...');
|
|
||||||
|
|
||||||
if (!window.FocusVideoPlayer) {
|
|
||||||
log('❌ FocusVideoPlayer class not found!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
focusPlayer = new FocusVideoPlayer('#focus-video-container');
|
|
||||||
log('✅ FocusVideoPlayer instance created successfully');
|
|
||||||
|
|
||||||
// Initialize with mock video manager
|
|
||||||
focusPlayer.initializeVideoLibrary(window.videoPlayerManager);
|
|
||||||
log('📚 Video library initialized');
|
|
||||||
|
|
||||||
// Start focus session
|
|
||||||
focusPlayer.startFocusSession();
|
|
||||||
log('🎬 Focus session started');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ FocusVideoPlayer test failed: ${error.message}`);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopFocusPlayer() {
|
|
||||||
if (focusPlayer) {
|
|
||||||
focusPlayer.stopFocusSession();
|
|
||||||
log('🛑 Focus session stopped');
|
|
||||||
} else {
|
|
||||||
log('⚠️ No focus player to stop');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on page load
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
log('🚀 Video Player Test Page Loaded');
|
|
||||||
log('📋 Available classes:');
|
|
||||||
log(` - BaseVideoPlayer: ${window.BaseVideoPlayer ? '✅' : '❌'}`);
|
|
||||||
log(` - FocusVideoPlayer: ${window.FocusVideoPlayer ? '✅' : '❌'}`);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue