7207 lines
359 KiB
HTML
7207 lines
359 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: file: blob: http://localhost:* https:; connect-src 'self' http://localhost:* https: ws://localhost:*; img-src 'self' data: file: blob:; media-src 'self' data: file: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;">
|
|
<title>Gooner Training Academy - Master Your Dedication</title>
|
|
<link rel="stylesheet" href="src/styles/color-variables.css">
|
|
<link rel="stylesheet" href="src/styles/styles.css">
|
|
<link rel="stylesheet" href="src/styles/base-video-player.css">
|
|
<link rel="stylesheet" href="src/styles/overlay-video-player.css">
|
|
<link href="https://fonts.googleapis.com/css2?family=Audiowide&family=Michroma&family=Electrolize:wght@400&display=swap" rel="stylesheet">
|
|
<style>
|
|
/* Verification Photo Styles */
|
|
.verification-photo-info {
|
|
border-left: 4px solid var(--color-error);
|
|
background: var(--bg-primary-overlay-10);
|
|
padding: 8px !important;
|
|
}
|
|
|
|
.verification-type {
|
|
color: var(--color-error);
|
|
font-weight: bold;
|
|
font-size: 0.9em;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.verification-message {
|
|
color: var(--color-error);
|
|
font-style: italic;
|
|
font-size: 0.8em;
|
|
margin: 4px 0;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.verification-timestamp {
|
|
color: var(--text-dim);
|
|
font-size: 0.75em;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.verification-modal .verification-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.verification-modal .verification-details {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.verification-message-display {
|
|
background: var(--bg-primary-overlay-10);
|
|
border: 1px solid var(--color-error);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.verification-message-display h4 {
|
|
color: var(--color-error);
|
|
margin: 0 0 10px 0;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.verification-message-display p {
|
|
color: var(--color-error);
|
|
font-style: italic;
|
|
margin: 0;
|
|
font-size: 1.1em;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Gallery Verification Photo Styles */
|
|
.verification-photo-item {
|
|
border: 2px solid var(--color-error);
|
|
border-radius: 8px;
|
|
background: var(--bg-primary-overlay-10);
|
|
}
|
|
|
|
.verification-photo-container {
|
|
background: var(--bg-primary-overlay-10);
|
|
}
|
|
|
|
.verification-photo-info {
|
|
background: var(--bg-primary-overlay-20);
|
|
border-top: 1px solid var(--color-error);
|
|
padding: 8px !important;
|
|
}
|
|
|
|
.verification-photo-info .photo-type {
|
|
color: var(--color-error);
|
|
font-weight: bold;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.verification-photo-info .verification-message {
|
|
display: block;
|
|
color: var(--color-error);
|
|
font-style: italic;
|
|
font-size: 0.8em;
|
|
margin-top: 4px;
|
|
line-height: 1.2;
|
|
max-width: 100%;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.verification-photo-select:checked + .checkbox-label {
|
|
background-color: var(--color-error);
|
|
border-color: var(--color-error);
|
|
}
|
|
</style>
|
|
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js" crossorigin="anonymous"></script>
|
|
<script>
|
|
// Test JSZip availability when page loads
|
|
window.addEventListener('load', function() {
|
|
if (typeof JSZip !== 'undefined') {
|
|
console.log('? JSZip loaded successfully from global scope');
|
|
} else if (typeof window.JSZip !== 'undefined') {
|
|
console.log('? JSZip loaded successfully from window scope');
|
|
} else {
|
|
console.error('? JSZip failed to load');
|
|
}
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<!-- Loading Overlay -->
|
|
<div id="loading-overlay" class="loading-overlay">
|
|
<div class="loading-content">
|
|
<div class="loading-spinner"></div>
|
|
<h2>Initializing Game...</h2>
|
|
<p class="loading-status" id="loading-status">Loading components...</p>
|
|
<div class="loading-progress">
|
|
<div class="loading-progress-fill" id="loading-progress-fill"></div>
|
|
</div>
|
|
<div class="loading-percentage" id="loading-percentage">0%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Side Characters - Outside game-container for fixed positioning -->
|
|
<div class="character-side left" style="background-image: url('assets/hentai/1.png'); top: 10%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/11.png'); top: 15%;"></div>
|
|
<div class="character-side left" style="background-image: url('assets/hentai/3.png'); top: 30%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/4.png'); top: 35%;"></div>
|
|
<div class="character-side left" style="background-image: url('assets/hentai/5.png'); top: 50%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/6.png'); top: 55%;"></div>
|
|
<div class="character-side left" style="background-image: url('assets/hentai/7.png'); top: 70%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/8.png'); top: 75%;"></div>
|
|
<div class="character-side left" style="background-image: url('assets/hentai/9.png'); top: 90%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/10.png'); top: 95%;"></div>
|
|
<div class="character-side left" style="background-image: url('assets/hentai/12.png'); top: 110%;"></div>
|
|
<div class="character-side right" style="background-image: url('assets/hentai/13.png'); top: 115%;"></div>
|
|
|
|
<div class="game-container">
|
|
|
|
<!-- Game Header -->
|
|
<header class="game-header hero-header">
|
|
<div class="hero-background"></div>
|
|
<div class="neon-grid"></div>
|
|
|
|
<!-- Utility Navigation -->
|
|
<div class="hero-utility-nav">
|
|
<!-- User Profile Display -->
|
|
<div class="user-profile-display">
|
|
<div class="profile-avatar-small" id="header-profile-avatar">
|
|
<div class="avatar-placeholder">👤</div>
|
|
</div>
|
|
<div class="profile-info">
|
|
<div class="profile-username" id="header-username">Player</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Panel -->
|
|
<div class="hero-status-panel">
|
|
<!-- Level and XP Display -->
|
|
<div class="status-card level-display">
|
|
<div class="status-label">Current Level</div>
|
|
<div class="status-value level-info">
|
|
<div class="level-title" id="current-level-name">Virgin</div>
|
|
<div class="level-number">Level <span id="current-level-number">1</span></div>
|
|
</div>
|
|
<div class="level-xp-info">
|
|
<div class="xp-display">
|
|
<span id="level-xp-header" class="xp-number">0</span>
|
|
<span class="xp-suffix">XP</span>
|
|
</div>
|
|
<div class="level-progress-container">
|
|
<div class="status-bar">
|
|
<div class="status-progress" id="level-xp-progress" style="width: 0%"></div>
|
|
</div>
|
|
<div class="level-progress-text">
|
|
<span id="level-progress-current">0</span> / <span id="level-progress-next">100</span> XP
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="user-profile-btn" class="utility-btn" title="View Profile">
|
|
<span class="utility-icon">👤</span>
|
|
<span class="utility-label">Profile</span>
|
|
</button>
|
|
<button id="game-guide-btn" class="utility-btn" title="Game Guide">
|
|
<span class="utility-icon">📖</span>
|
|
<span class="utility-label">Guide</span>
|
|
</button>
|
|
<button id="options-menu-btn" class="utility-btn" title="Options">
|
|
<span class="utility-icon">⚙️</span>
|
|
<span class="utility-label">Options</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="hero-content">
|
|
<div class="hero-title-section">
|
|
<h1 class="hero-title">
|
|
<span class="hero-title-main">Gooner Training Academy</span>
|
|
<span class="hero-title-accent">Professional Development</span>
|
|
</h1>
|
|
<p class="hero-tagline">
|
|
<span class="tagline-emphasis">Master Your Dedication</span>
|
|
<span class="tagline-subtitle">★ Advanced Training System ★ v0.5</span>
|
|
</p>
|
|
|
|
<!-- Feature Highlights - Now Main Action Buttons -->
|
|
<div class="hero-features">
|
|
<button class="hero-feature btn-feature" id="campaign-btn">
|
|
<span class="feature-icon">🎯</span>
|
|
<span class="feature-text">Campaign Mode</span>
|
|
</button>
|
|
<button class="hero-feature btn-feature" id="training-academy-btn">
|
|
<span class="feature-icon">🎓</span>
|
|
<span class="feature-text">Training Academy</span>
|
|
</button>
|
|
<button class="hero-feature btn-feature" id="quick-play-btn">
|
|
<span class="feature-icon">⚡</span>
|
|
<span class="feature-text">Quick Play</span>
|
|
</button>
|
|
<button class="hero-feature btn-feature" id="porn-cinema-btn">
|
|
<span class="feature-icon">🎬</span>
|
|
<span class="feature-text">Porn Cinema</span>
|
|
</button>
|
|
<button class="hero-feature btn-feature" id="hypno-gallery-btn">
|
|
<span class="feature-icon">🌀</span>
|
|
<span class="feature-text">Hypno Gallery</span>
|
|
</button>
|
|
<button class="hero-feature btn-feature" id="library-btn" onclick="window.location.href='library.html'">
|
|
<span class="cassie-icon"></span>
|
|
<span class="feature-text">Library</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cyberpunk Video Billboard -->
|
|
<div class="video-billboard-container">
|
|
<div class="video-billboard">
|
|
<div class="billboard-frame">
|
|
<video id="billboard-video" muted playsinline preload="metadata">
|
|
<source id="billboard-video-source" src="" type="video/mp4">
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
<div class="billboard-overlay">
|
|
<div class="billboard-controls">
|
|
<button id="billboard-mute" class="billboard-btn" title="Toggle Mute"></button>
|
|
<button id="billboard-pause" class="billboard-btn" title="Toggle Pause"></button>
|
|
</div>
|
|
</div>
|
|
<div class="neon-border">
|
|
<div class="neon-edge top"></div>
|
|
<div class="neon-edge bottom"></div>
|
|
<div class="neon-edge left"></div>
|
|
<div class="neon-edge right"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compact Music Controls (repositioned) -->
|
|
<div class="music-controls-compact hero-music-controls">
|
|
<button id="music-toggle-compact" class="music-icon-btn" title="Music Controls">🎵</button>
|
|
<div class="music-panel-expanded">
|
|
<div class="music-row">
|
|
<button id="music-toggle" class="music-btn-small" title="Play/Pause">⏯️</button>
|
|
<button id="loop-btn" class="music-btn-small" title="Loop">🔁</button>
|
|
<button id="shuffle-btn" class="music-btn-small" title="Shuffle">🔀</button>
|
|
</div>
|
|
<div class="music-row">
|
|
<select id="track-selector" class="track-dropdown-compact">
|
|
<option value="0">Colorful Flowers</option>
|
|
<option value="1">New Beginnings</option>
|
|
<option value="2">Storm Clouds</option>
|
|
<option value="3">Brunch For Two</option>
|
|
</select>
|
|
</div>
|
|
<div class="music-row">
|
|
<div class="volume-control-compact">
|
|
<span class="volume-icon">🔊</span>
|
|
<input type="range" id="volume-slider" min="0" max="100" value="30" class="volume-slider-compact">
|
|
<span class="volume-percent" id="volume-percent">30%</span>
|
|
</div>
|
|
</div>
|
|
<div class="music-status-compact" id="music-status">Music: Off</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Game Content -->
|
|
<main class="game-content">
|
|
<!-- Start Screen -->
|
|
<div id="start-screen" class="screen active">
|
|
<!-- Game Guide and Options moved to end of body for proper z-index stacking -->
|
|
|
|
</div>
|
|
|
|
<!-- Image Management Screen -->
|
|
<div id="image-management-screen" class="screen">
|
|
<h2>? Image Library Management</h2>
|
|
<p>Upload and organize image content to enhance your gaming experience</p>
|
|
|
|
<!-- Upload Section -->
|
|
<div class="upload-section">
|
|
<h3> Import Image Files</h3>
|
|
<div class="upload-controls">
|
|
<button id="import-task-images-btn" class="btn btn-primary"> Task Images</button>
|
|
<button id="import-consequence-images-btn" class="btn btn-warning"> Consequence Images</button>
|
|
<input type="file" id="image-upload-input" accept="image/*" multiple style="display: none;">
|
|
</div>
|
|
<div class="upload-info desktop-feature">
|
|
<span> Desktop: Native file dialogs � Supports JPEG, PNG, GIF, WebP formats</span>
|
|
</div>
|
|
<div class="upload-info web-feature" style="display: none;">
|
|
<span> Web: Limited browser upload functionality</span>
|
|
</div>
|
|
<div class="directory-controls">
|
|
<button id="storage-info-btn" class="btn btn-outline"> Storage Info</button>
|
|
<button id="cleanup-invalid-images-btn" class="btn btn-warning"> Cleanup</button>
|
|
<button id="clear-all-images-btn" class="btn btn-danger">? Clear All</button>
|
|
<span class="scan-info"> Auto-scan on startup</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Gallery Section -->
|
|
<div class="gallery-section">
|
|
<div class="gallery-header">
|
|
<h3>? Current Image Library</h3>
|
|
<div class="gallery-controls">
|
|
<!-- Selection buttons removed - lib-image-gallery uses click-to-preview instead of checkboxes -->
|
|
<button id="refresh-images-btn" class="btn btn-secondary btn-small"> Refresh</button>
|
|
<span class="image-count">Loading images...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Task/Consequence image galleries removed - consolidated into lib-image-gallery in Library section -->
|
|
</div>
|
|
|
|
<!-- Image Preview Section -->
|
|
<div class="image-preview-section" id="image-preview-section" style="display: none;">
|
|
<h4>? Image Preview</h4>
|
|
<div class="preview-controls">
|
|
<div class="image-preview-container">
|
|
<img id="image-preview-img" src="" alt="Image Preview" style="max-width: 100%; max-height: 600px; border-radius: 8px;">
|
|
</div>
|
|
<div class="preview-info">
|
|
<span id="preview-image-name">No image selected</span>
|
|
<div class="preview-buttons">
|
|
<button id="close-image-preview-btn" class="btn btn-small btn-outline">? Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="management-buttons">
|
|
<button id="back-to-start-from-images-btn" class="btn btn-secondary">? Back to Start</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio Management Screen -->
|
|
<div id="audio-management-screen" class="screen">
|
|
<h2> Audio Library Management</h2>
|
|
<p>Upload and organize audio content to enhance your gaming experience</p>
|
|
|
|
<!-- Upload Section -->
|
|
<div class="upload-section">
|
|
<h3> Import Audio Files</h3>
|
|
<div class="upload-controls">
|
|
<button id="import-background-audio-btn" class="btn btn-primary"> Background Music</button>
|
|
<button id="import-ambient-audio-btn" class="btn btn-success"> Ambient Sounds</button>
|
|
<input type="file" id="audio-upload-input" accept="audio/*" multiple style="display: none;">
|
|
</div>
|
|
<div class="upload-info desktop-feature">
|
|
<span> Desktop: Native file dialogs � Supports MP3, WAV, OGG, M4A, AAC, FLAC formats</span>
|
|
</div>
|
|
<div class="upload-info web-feature" style="display: none;">
|
|
<span> Web: Limited browser upload functionality</span>
|
|
</div>
|
|
<div class="directory-controls">
|
|
<button id="scan-audio-directories-btn" class="btn btn-primary"> Scan Directories</button>
|
|
<button id="audio-storage-info-btn" class="btn btn-outline"> Storage Info</button>
|
|
<button id="cleanup-invalid-audio-btn" class="btn btn-warning"> Cleanup</button>
|
|
<button id="clear-all-audio-btn" class="btn btn-danger">? Clear All</button>
|
|
<span class="scan-info"> Auto-scan on startup</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio Gallery Section -->
|
|
<div class="gallery-section">
|
|
<div class="gallery-header">
|
|
<h3> Current Audio Library</h3>
|
|
<div class="gallery-controls">
|
|
<!-- Selection buttons removed - lib-audio-gallery uses click-to-play instead of checkboxes -->
|
|
<span class="audio-count">Loading audio files...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio tabs and galleries removed - using lib-audio-gallery in Library section instead -->
|
|
</div>
|
|
|
|
<!-- Audio Preview Section -->
|
|
<div class="audio-preview-section" id="audio-preview-section" style="display: none;">
|
|
<h4> Audio Preview</h4>
|
|
<div class="preview-controls">
|
|
<div class="audio-preview-container">
|
|
<audio id="audio-preview-player" controls style="width: 100%; max-width: 600px;">
|
|
Your browser does not support the audio element.
|
|
</audio>
|
|
</div>
|
|
<div class="preview-info">
|
|
<span id="preview-file-name">No file selected</span>
|
|
<div class="preview-buttons">
|
|
<button id="close-preview-btn" class="btn btn-small btn-outline">? Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="management-buttons">
|
|
<button id="back-to-start-from-audio-btn" class="btn btn-secondary">? Back to Start</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Management Screen -->
|
|
<div id="video-management-screen" class="screen">
|
|
<h2> Video Library Management</h2>
|
|
<p>Upload and organize video content to enhance your gaming experience</p>
|
|
|
|
<!-- Upload Section -->
|
|
<div class="upload-section">
|
|
<h3> Video Library Management</h3>
|
|
<p style="color: var(--text-dim); font-size: 12px; margin-bottom: 15px;">
|
|
Link external directories to build your video library. All videos are scanned recursively (including subdirectories).
|
|
</p>
|
|
|
|
<!-- Directory Management Section -->
|
|
<div class="directory-management-section" style="margin-bottom: 20px; padding: 15px; background: var(--bg-secondary); border-radius: 8px;">
|
|
<h4>? 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"> 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: var(--bg-tertiary); border-radius: 4px; text-align: center;">
|
|
<strong id="total-video-count">0 videos total</strong>
|
|
<span id="directory-count" style="color: var(--text-muted); margin-left: 10px;">0 directories linked</span>
|
|
</div>
|
|
<div class="upload-info desktop-feature">
|
|
<span> Desktop: Native file dialogs � Supports MP4, WebM, OGV, MOV formats</span>
|
|
</div>
|
|
<div class="upload-info web-feature" style="display: none;">
|
|
<span> Web: Limited browser upload functionality</span>
|
|
</div>
|
|
<div class="directory-controls">
|
|
<button id="video-storage-info-btn" class="btn btn-outline"> Storage Info</button>
|
|
<button id="cleanup-invalid-videos-btn" class="btn btn-warning"> Cleanup</button>
|
|
<button id="clear-all-videos-btn" class="btn btn-danger">? Clear All</button>
|
|
<span class="scan-info"> Auto-scan on startup</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Gallery Section -->
|
|
<div class="gallery-section">
|
|
<div class="gallery-header">
|
|
<h3> Video Library</h3>
|
|
<div class="gallery-controls">
|
|
<!-- Selection buttons removed - lib-video-gallery uses click-to-preview instead of checkboxes -->
|
|
<button id="refresh-videos-btn" class="btn btn-secondary btn-small"> Refresh</button>
|
|
<span class="video-count">Loading videos...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified video gallery removed - using lib-video-gallery in Library section instead -->
|
|
</div>
|
|
|
|
<!-- Video Preview Section -->
|
|
<div class="video-preview-section" id="video-preview-section" style="display: none;">
|
|
<h4> Video Preview</h4>
|
|
<div class="preview-controls">
|
|
<div class="video-preview-container">
|
|
<video id="video-preview-player" controls style="width: 100%; max-width: 800px; max-height: 450px; border-radius: 8px;">
|
|
Your browser does not support the video element.
|
|
</video>
|
|
</div>
|
|
<div class="preview-info">
|
|
<span id="preview-video-name">No video selected</span>
|
|
<div class="preview-buttons">
|
|
<button id="toggle-video-loop-btn" class="btn btn-small btn-outline"> Loop</button>
|
|
<button id="close-video-preview-btn" class="btn btn-small btn-outline">? Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Settings Section -->
|
|
<div class="video-settings-section">
|
|
<h4> Video Player Settings</h4>
|
|
<div class="settings-grid">
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="enable-video-player" checked>
|
|
<span class="switch"></span>
|
|
Enable Video Player
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="enable-background-videos" checked>
|
|
<span class="switch"></span>
|
|
Background Videos
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="enable-task-videos" checked>
|
|
<span class="switch"></span>
|
|
Task Videos
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group volume-setting">
|
|
<label for="video-volume">Video Volume:</label>
|
|
<div class="volume-control">
|
|
<input type="range" id="video-volume" min="0" max="100" value="30" class="volume-slider">
|
|
<span id="video-volume-display">30%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="video-autoplay" checked>
|
|
<span class="switch"></span>
|
|
Autoplay Videos
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="video-show-controls">
|
|
<span class="switch"></span>
|
|
Show Video Controls
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label class="switch-label">
|
|
<input type="checkbox" id="video-fade-transitions" checked>
|
|
<span class="switch"></span>
|
|
Fade Transitions
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-buttons">
|
|
<button id="test-background-video" class="btn btn-info">Test Background Video</button>
|
|
<button id="test-overlay-video" class="btn btn-primary">Test Overlay Video</button>
|
|
<button id="stop-all-videos" class="btn btn-danger">Stop All Videos</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="management-buttons">
|
|
<button id="back-to-start-from-video-btn" class="btn btn-secondary">Back to Start</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Gallery Screen -->
|
|
<div id="photo-gallery-screen" class="screen">
|
|
<h2> Photo Gallery Management</h2>
|
|
<p>View and manage your captured photos from photography sessions</p>
|
|
|
|
<!-- Upload Section -->
|
|
<div class="upload-section">
|
|
<h3> Photo Management</h3>
|
|
<div class="upload-controls">
|
|
<button id="download-all-photos-btn" class="btn btn-primary"> Download All Photos</button>
|
|
<button id="download-selected-photos-btn" class="btn btn-info" disabled> Download Selected</button>
|
|
<button id="clear-all-photos-btn" class="btn btn-danger">? Clear All Photos</button>
|
|
<button id="photo-storage-settings-btn" class="btn btn-secondary"> Storage Settings</button>
|
|
</div>
|
|
<div class="upload-info desktop-feature">
|
|
<span> Desktop: Photos automatically saved from sessions � JPEG, PNG formats</span>
|
|
</div>
|
|
<div class="directory-controls">
|
|
<div class="photo-stats">
|
|
<span id="photo-count-display">Loading photos...</span>
|
|
<span id="storage-size-display"></span>
|
|
</div>
|
|
<span class="scan-info"> Photos saved from gameplay sessions</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Gallery Section -->
|
|
<div class="gallery-section">
|
|
<div class="gallery-header">
|
|
<h3> Photo Collection</h3>
|
|
<div class="gallery-controls">
|
|
<button id="select-all-photos-btn" class="btn btn-small btn-outline">Select All</button>
|
|
<button id="deselect-all-photos-btn" class="btn btn-small btn-outline">Deselect All</button>
|
|
<div class="filter-controls">
|
|
<label>Filter by Session:
|
|
<select id="photo-session-filter" class="filter-select">
|
|
<option value="all">All Photos</option>
|
|
<option value="dress-up-photo">Dress-up Photos</option>
|
|
<option value="photography-studio">Photography Studio</option>
|
|
<option value="custom">Custom Sessions</option>
|
|
</select>
|
|
</label>
|
|
<label>Sort by:
|
|
<select id="photo-sort-order" class="filter-select">
|
|
<option value="newest">Newest First</option>
|
|
<option value="oldest">Oldest First</option>
|
|
<option value="session">By Session Type</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Tabs -->
|
|
<div class="photo-tabs">
|
|
<button id="all-photos-tab" class="tab-btn active"> All Photos</button>
|
|
<button id="dress-up-photos-tab" class="tab-btn"> Dress-up</button>
|
|
<button id="studio-photos-tab" class="tab-btn"> Studio</button>
|
|
<button id="custom-photos-tab" class="tab-btn"> Custom</button>
|
|
</div>
|
|
|
|
<div class="photo-galleries-container">
|
|
<div id="all-photos-gallery" class="photo-gallery active">
|
|
<div id="photo-grid" class="photo-grid">
|
|
<!-- All photos will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dress-up-photos-gallery" class="photo-gallery">
|
|
<div class="photo-grid">
|
|
<!-- Dress-up photos will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="studio-photos-gallery" class="photo-gallery">
|
|
<div class="photo-grid">
|
|
<!-- Studio photos will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="custom-photos-gallery" class="photo-gallery">
|
|
<div class="photo-grid">
|
|
<!-- Custom photos will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="no-photos-message" class="no-photos-message" style="display: none;">
|
|
<h4> No Photos Yet</h4>
|
|
<p>Take some photos during photography sessions and they'll appear here!</p>
|
|
<p>You can enable photo saving in the settings when you take your first photo.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Preview Section -->
|
|
<div class="photo-preview-section" id="photo-preview-section" style="display: none;">
|
|
<h4> Photo Preview</h4>
|
|
<div class="preview-controls">
|
|
<div class="photo-preview-container">
|
|
<img id="photo-preview-image" src="" alt="Photo Preview" style="max-width: 100%; max-height: 600px; border-radius: 8px;">
|
|
</div>
|
|
<div class="preview-info">
|
|
<span id="preview-photo-name">No photo selected</span>
|
|
<div class="preview-buttons">
|
|
<button id="download-photo-btn" class="btn btn-small btn-primary"> Download</button>
|
|
<button id="close-photo-preview-btn" class="btn btn-small btn-outline">? Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Detail Modal -->
|
|
<div id="photo-detail-modal" class="modal photo-modal" style="display: none;">
|
|
<div class="modal-content photo-detail-content">
|
|
<div class="modal-header">
|
|
<h3 id="photo-detail-title">Photo Details</h3>
|
|
<span class="close" id="close-photo-detail">×</span>
|
|
</div>
|
|
<div class="modal-body photo-detail-body">
|
|
<div class="photo-display">
|
|
<img id="photo-detail-image" src="" alt="Photo" />
|
|
</div>
|
|
<div class="photo-info">
|
|
<div class="info-row">
|
|
<label>Captured:</label>
|
|
<span id="photo-detail-date"></span>
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Session:</label>
|
|
<span id="photo-detail-session"></span>
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Task:</label>
|
|
<span id="photo-detail-task"></span>
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Size:</label>
|
|
<span id="photo-detail-size"></span>
|
|
</div>
|
|
</div>
|
|
<div class="photo-actions">
|
|
<button id="download-photo-btn" class="btn btn-primary"> Download</button>
|
|
<button id="delete-photo-btn" class="btn btn-danger">? Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Storage Settings Modal -->
|
|
<div id="photo-settings-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3> Photo Storage Settings</h3>
|
|
<span class="close" id="close-photo-settings">×</span>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="setting-group">
|
|
<h4>Storage Consent</h4>
|
|
<div class="consent-controls">
|
|
<label>
|
|
<input type="radio" name="photo-consent" value="true" id="consent-enable">
|
|
Enable photo storage (save photos for later viewing)
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="photo-consent" value="false" id="consent-disable">
|
|
Disable photo storage (session only)
|
|
</label>
|
|
</div>
|
|
<p class="help-text">
|
|
Photos are stored locally on your device only. No photos are sent to any servers.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<h4>Storage Information</h4>
|
|
<div class="storage-info">
|
|
<div class="info-item">
|
|
<span class="info-label">Total Photos:</span>
|
|
<span id="settings-photo-count">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">Storage Used:</span>
|
|
<span id="settings-storage-size">0 KB</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">Oldest Photo:</span>
|
|
<span id="settings-oldest-photo">None</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button id="save-photo-settings-btn" class="btn btn-primary">Save Settings</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="management-buttons">
|
|
<button id="back-to-start-from-photos-btn" class="btn btn-secondary">? Back to Start</button>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Game Data System -->
|
|
<script src="src/data/modes/mainGameData.js"></script>
|
|
<script src="src/data/modes/humiliationGameData.js"></script>
|
|
<script src="src/data/modes/trainingGameData.js"></script>
|
|
<script src="src/data/modes/enduranceGameData.js"></script>
|
|
<script src="src/data/modes/dressUpGameData.js"></script>
|
|
<script src="src/data/gameDataManager.js"></script>
|
|
|
|
<!-- Inventory System Utilities -->
|
|
<script src="src/utils/inventoryManager.js"></script>
|
|
<script src="src/data/poseBanks/inventoryPoseBank.js"></script>
|
|
|
|
<!-- Backup System -->
|
|
<script src="src/utils/backupManager.js"></script>
|
|
|
|
<!-- Theme Manager -->
|
|
<script src="src/utils/themeManager.js"></script>
|
|
|
|
<!-- Legacy Data (will be phased out) -->
|
|
<script src="src/data/gameData.js"></script>
|
|
|
|
<!-- Statistics Modal -->
|
|
<div id="stats-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2> Game Statistics</h2>
|
|
<span class="close" id="close-stats">×</span>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>Games Played</h3>
|
|
<div class="stat-value" id="stat-games">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Tasks Completed</h3>
|
|
<div class="stat-value" id="stat-completed">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Best Score</h3>
|
|
<div class="stat-value" id="stat-score">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Current Streak</h3>
|
|
<div class="stat-value" id="stat-streak">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Completion Rate</h3>
|
|
<div class="stat-value" id="stat-rate">0%</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Hours Played</h3>
|
|
<div class="stat-value" id="stat-hours">0.0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-actions">
|
|
<button id="reset-stats-btn" class="btn btn-warning">Reset Statistics</button>
|
|
<button id="export-stats-btn" class="btn btn-secondary">
|
|
<span class="btn-text">Export Stats Only</span>
|
|
<span class="btn-loading" style="display: none;">? Exporting...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Help Menu Modal -->
|
|
<div id="help-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2> Keyboard Shortcuts & Help</h2>
|
|
<span class="close" id="close-help">×</span>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="help-section">
|
|
<h3> Game Controls</h3>
|
|
<div class="shortcut-list">
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">Enter</span>
|
|
<span class="shortcut-action">Complete Current Task</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">Ctrl</span>
|
|
<span class="shortcut-action">Skip Task (get consequence task)</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">Shift+Ctrl</span>
|
|
<span class="shortcut-action">Mercy Skip (spend points to avoid consequence)</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">Space</span>
|
|
<span class="shortcut-action">Pause/Resume Game</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">P</span>
|
|
<span class="shortcut-action">Pause/Resume Game</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="help-section">
|
|
<h3> Music Controls</h3>
|
|
<div class="shortcut-list">
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">M</span>
|
|
<span class="shortcut-action">Toggle Music On/Off</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="help-section">
|
|
<h3> Navigation</h3>
|
|
<div class="shortcut-list">
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">Escape</span>
|
|
<span class="shortcut-action">Close Modals / Back / Pause Game</span>
|
|
</div>
|
|
<div class="shortcut-item">
|
|
<span class="shortcut-key">H</span>
|
|
<span class="shortcut-action">Show/Hide This Help Menu</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="help-section">
|
|
<h3> Tips</h3>
|
|
<ul class="help-tips">
|
|
<li>Use Enter and Ctrl for lightning-fast task progression</li>
|
|
<li>Ctrl gives you a consequence task - complete it to save points!</li>
|
|
<li>Shift+Ctrl spends points for mercy skip - use strategically</li>
|
|
<li>Choose your skipping strategy: face the consequence or pay points</li>
|
|
<li>Shortcuts are disabled when typing in input fields</li>
|
|
<li>The game auto-saves when paused and offers to resume</li>
|
|
<li>Export your progress regularly to avoid data loss</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options Popup -->
|
|
<div id="options-backdrop" class="popup-backdrop" style="display: none;"></div>
|
|
<div id="options-menu" class="options-menu" style="display: none;">
|
|
<button class="popup-close" id="options-close" title="Close">×</button>
|
|
<h3 style="color: var(--color-primary); margin-bottom: var(--space-lg); margin-top: 0; text-align: center; font-size: 1.5em;"> Options</h3>
|
|
<!-- Image Theme Selector -->
|
|
<div class="option-item theme-selector">
|
|
<label for="theme-dropdown" class="option-label">Choose Your Image Theme:</label>
|
|
<select id="theme-dropdown" class="theme-dropdown">
|
|
<option value="hentai" selected> Hentai</option>
|
|
<option value="pornstars">? Pornstars</option>
|
|
<option value="BBC"> BBC</option>
|
|
<option value="feet"> Feet</option>
|
|
<option value="library"> Library (Random)</option>
|
|
<option value="none"> None</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Color Theme Selector -->
|
|
<div class="option-item theme-selector">
|
|
<label for="color-theme-dropdown" class="option-label"> Choose Your Color Theme:</label>
|
|
<select id="color-theme-dropdown" class="theme-dropdown">
|
|
<option value="electric-blue">? Electric Blue</option>
|
|
<option value="electric-violet"> Electric Violet</option>
|
|
<option value="magenta-dream"> Magenta Dream</option>
|
|
<option value="hot-pink-fury"> Hot Pink Fury</option>
|
|
<option value="purple-neon-dream"> Purple Neon Dream</option>
|
|
<option value="crimson-passion"> Crimson Passion</option>
|
|
<option value="golden-ember">? Golden Ember</option>
|
|
<option value="aqua-mist"> Aqua Mist</option>
|
|
<option value="coral-blaze"> Coral Blaze</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<input type="file" id="import-file" accept=".json" style="display: none;">
|
|
|
|
<!-- Game Guide Popup - at body level for proper z-index -->
|
|
<div id="game-guide-backdrop" class="popup-backdrop" style="display: none;"></div>
|
|
<div id="game-guide" class="game-guide" style="display: none;">
|
|
<button class="popup-close" id="game-guide-close" title="Close">×</button>
|
|
<div class="guide-content">
|
|
<h3> Game Modes Overview</h3>
|
|
|
|
<div class="guide-section">
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Training Academy</h4>
|
|
<p>Immersive scenario-based training sessions with branching storylines. Earn XP through time-based rewards (1 XP/2min), webcam activities (1 XP/min), and photo sessions (2 XP/photo).</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Quick Play</h4>
|
|
<p>Fast-paced task-based gameplay with real-time XP tracking. Complete tasks to earn 1-3 XP based on completion time, plus session bonuses every 15 minutes (2 XP + 1 XP if recording).</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Porn Cinema</h4>
|
|
<p>Video viewing experience with XP rewards. Earn 1 XP per 5 minutes of viewing time. Videos must reach 90% completion to count toward your stats and achievements.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Hypno Gallery</h4>
|
|
<p>Immersive slideshow experience with configurable timing modes, visual effects, and transitions. View your image library in constant, random, or wave timing patterns with customizable durations and effects.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Library</h4>
|
|
<p>Manage your video and photo collections. Organize files, create playlists, and browse your media library. Essential for setting up content for other game modes.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Profile</h4>
|
|
<p>View your progress, stats, and achievements. Track your level, XP breakdown by game mode, session history, and unlock status. Monitor your gooning journey!</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-item">
|
|
<div class="guide-icon"></div>
|
|
<div class="guide-info">
|
|
<h4>Options</h4>
|
|
<p>Customize your experience with themes, popup settings, and debug tools. Switch between Hentai/Pornstar themes, adjust popup intervals, and manage system settings.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="guide-tips">
|
|
<h4> Pro Tips</h4>
|
|
<ul>
|
|
<li><strong>Level Up:</strong> Combine different game modes to maximize XP earning</li>
|
|
<li><strong>Recording Bonus:</strong> Enable session recording in Quick Play for extra XP</li>
|
|
<li><strong>Consistency:</strong> Daily play builds streaks and unlocks achievements</li>
|
|
<li><strong>Library First:</strong> Set up your media library before starting sessions</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
<!-- Script Loading Order -->
|
|
<script src="src/features/ui/flashMessageManager.js"></script>
|
|
<script src="src/features/images/popupImageManager.js"></script>
|
|
|
|
<script src="src/features/audio/audioManager.js"></script>
|
|
<script src="src/features/stats/playerStats.js"></script>
|
|
<script src="src/core/gameModeManager.js"></script>
|
|
<script src="src/features/webcam/webcamManager.js"></script>
|
|
<script src="src/features/tts/voiceManager.js"></script>
|
|
<script src="src/features/media/baseVideoPlayer.js"></script>
|
|
<script src="src/features/media/focusVideoPlayer.js"></script>
|
|
<script src="src/features/media/overlayVideoPlayer.js"></script>
|
|
<script src="src/features/media/quadVideoPlayer.js"></script>
|
|
<script src="src/features/tasks/interactiveTaskManager.js"></script>
|
|
<script src="src/features/video/videoPlayerManager.js"></script>
|
|
<script src="src/utils/desktop-file-manager.js"></script>
|
|
<script src="src/core/game.js"></script>
|
|
<script>
|
|
// Theme switching functionality
|
|
async function applyTheme(theme) {
|
|
console.log('Applying theme:', theme);
|
|
const characterSides = document.querySelectorAll('.character-side');
|
|
|
|
// Remove any existing theme classes
|
|
document.body.classList.remove('library-theme');
|
|
|
|
if (theme === 'none') {
|
|
characterSides.forEach(element => {
|
|
element.style.display = 'none';
|
|
});
|
|
localStorage.setItem('selectedTheme', theme);
|
|
return;
|
|
}
|
|
|
|
if (theme === 'library') {
|
|
// Add library theme class for CSS targeting
|
|
document.body.classList.add('library-theme');
|
|
// Use random images from user's library
|
|
await applyLibraryTheme(characterSides);
|
|
localStorage.setItem('selectedTheme', theme);
|
|
return;
|
|
}
|
|
|
|
// Handle asset themes (hentai, pornstars, BBC, feet) - scan directories dynamically
|
|
await applyAssetTheme(theme, characterSides);
|
|
|
|
// Save theme preference
|
|
localStorage.setItem('selectedTheme', theme);
|
|
}
|
|
|
|
async function applyAssetTheme(theme, characterSides) {
|
|
console.log(`Applying ${theme} theme with dynamic image scanning...`);
|
|
|
|
try {
|
|
let allImages = [];
|
|
|
|
// Try to scan the assets folder if we have Electron API
|
|
if (window.electronAPI && window.electronAPI.readDirectory) {
|
|
const themePath = `assets/${theme}`;
|
|
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i;
|
|
|
|
try {
|
|
let files = [];
|
|
const filesPromise = window.electronAPI.readDirectory(themePath);
|
|
|
|
if (filesPromise && typeof filesPromise.then === 'function') {
|
|
files = await filesPromise;
|
|
} else if (Array.isArray(filesPromise)) {
|
|
files = filesPromise;
|
|
}
|
|
|
|
if (files && files.length > 0) {
|
|
allImages = files
|
|
.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return imageExtensions.test(fileName);
|
|
})
|
|
.map(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return {
|
|
path: `${themePath}/${fileName}`,
|
|
name: fileName
|
|
};
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Could not scan ${themePath} directory:`, error);
|
|
}
|
|
}
|
|
|
|
console.log(` Found ${allImages.length} images in ${theme} theme`);
|
|
|
|
if (allImages.length === 0) {
|
|
// Fallback to hardcoded defaults if scanning failed
|
|
console.log('Using fallback hardcoded images');
|
|
applyFallbackTheme(theme, characterSides);
|
|
return;
|
|
}
|
|
|
|
// Apply dynamic layout like library theme
|
|
document.body.classList.add('library-theme');
|
|
|
|
const FIXED_WIDTH = 150;
|
|
const MIN_HEIGHT = 100;
|
|
const MAX_HEIGHT = 300;
|
|
const MARGIN = 0;
|
|
|
|
// Separate left and right columns
|
|
const leftElements = [];
|
|
const rightElements = [];
|
|
|
|
characterSides.forEach((element, index) => {
|
|
if (element.classList.contains('left')) {
|
|
leftElements.push(element);
|
|
} else if (element.classList.contains('right')) {
|
|
rightElements.push(element);
|
|
}
|
|
});
|
|
|
|
// Function to load and position images dynamically
|
|
const loadColumnImages = async (elements, isLeft) => {
|
|
let currentTop = 0;
|
|
const usedImages = new Set(); // Track used images to prevent duplicates
|
|
|
|
for (let i = 0; i < elements.length; i++) {
|
|
const element = elements[i];
|
|
|
|
// Get a random unused image
|
|
let randomImage;
|
|
let attempts = 0;
|
|
do {
|
|
const randomIndex = Math.floor(Math.random() * allImages.length);
|
|
randomImage = allImages[randomIndex];
|
|
attempts++;
|
|
} while (usedImages.has(randomImage.path) && attempts < allImages.length * 2);
|
|
|
|
if (randomImage) {
|
|
usedImages.add(randomImage.path); // Mark as used
|
|
const imageUrl = randomImage.path;
|
|
|
|
// Load image to get dimensions
|
|
const img = new Image();
|
|
await new Promise((resolve) => {
|
|
img.onload = () => {
|
|
// Calculate height based on aspect ratio
|
|
const aspectRatio = img.height / img.width;
|
|
let calculatedHeight = FIXED_WIDTH * aspectRatio;
|
|
|
|
// Clamp height to min/max bounds
|
|
calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, calculatedHeight));
|
|
|
|
// Apply styles to element
|
|
element.style.display = 'block';
|
|
element.style.width = `${FIXED_WIDTH}px`;
|
|
element.style.height = `${calculatedHeight}px`;
|
|
element.style.top = `${currentTop}px`;
|
|
element.style.backgroundImage = `url("${imageUrl}")`;
|
|
element.style.backgroundSize = 'cover';
|
|
element.style.backgroundPosition = 'center';
|
|
|
|
// Position left or right
|
|
if (isLeft) {
|
|
element.style.left = '0px';
|
|
element.style.right = 'auto';
|
|
} else {
|
|
element.style.right = '0px';
|
|
element.style.left = 'auto';
|
|
}
|
|
|
|
// Update currentTop for next image
|
|
currentTop += calculatedHeight + MARGIN;
|
|
|
|
console.log(` ${isLeft ? 'Left' : 'Right'} Panel ${i + 1}: ${randomImage.name} (${FIXED_WIDTH}x${calculatedHeight}px)`);
|
|
resolve();
|
|
};
|
|
|
|
img.onerror = () => {
|
|
console.warn(`Failed to load image: ${randomImage.path}`);
|
|
element.style.display = 'block';
|
|
element.style.width = `${FIXED_WIDTH}px`;
|
|
element.style.height = `${MIN_HEIGHT}px`;
|
|
element.style.top = `${currentTop}px`;
|
|
|
|
if (isLeft) {
|
|
element.style.left = '0px';
|
|
element.style.right = 'auto';
|
|
} else {
|
|
element.style.right = '0px';
|
|
element.style.left = 'auto';
|
|
}
|
|
|
|
currentTop += MIN_HEIGHT + MARGIN;
|
|
resolve();
|
|
};
|
|
|
|
img.src = imageUrl;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// Load both columns
|
|
await Promise.all([
|
|
loadColumnImages(leftElements, true),
|
|
loadColumnImages(rightElements, false)
|
|
]);
|
|
|
|
console.log('? Dynamic theme layout complete');
|
|
|
|
} catch (error) {
|
|
console.error('Error applying asset theme:', error);
|
|
applyFallbackTheme(theme, characterSides);
|
|
}
|
|
}
|
|
|
|
function applyFallbackTheme(theme, characterSides) {
|
|
// Fallback to original hardcoded approach
|
|
characterSides.forEach((element, index) => {
|
|
element.style.display = 'block';
|
|
|
|
let imageNames;
|
|
|
|
if (theme === 'hentai' || theme === 'pornstars') {
|
|
const imageNumbers = [1, 11, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
imageNames = imageNumbers.map(num => `${num}.png`);
|
|
} else if (theme === 'BBC') {
|
|
imageNames = [
|
|
'18808856_035_d374.png',
|
|
'19086746_049_ba0d.png',
|
|
'23515083_025_f794.png',
|
|
'24163996_041_81d6.png',
|
|
'31015308_126_3792.png',
|
|
'44141198_113_f31a.png',
|
|
'60016759_151_ea2b.png',
|
|
'85835876_037_0c84.png',
|
|
'99106223_020_819b.png',
|
|
'99769898_038_0e86.png'
|
|
];
|
|
} else if (theme === 'feet') {
|
|
imageNames = [
|
|
'20931683_001_c8f7.png',
|
|
'25674071_097_2016.png',
|
|
'29553632_079_53ec.png',
|
|
'30818412_002_8cbf.png',
|
|
'40352681_083_bec8.png',
|
|
'46780885_029_b8a2.png',
|
|
'47594084_126_51a7.png',
|
|
'67258313_065_60f1.png',
|
|
'79150090_035_3c99.png',
|
|
'80350346_043_3e8a.png'
|
|
];
|
|
}
|
|
|
|
if (imageNames && imageNames[index]) {
|
|
const imagePath = `assets/${theme}/${imageNames[index]}`;
|
|
element.style.backgroundImage = `url('${imagePath}')`;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function applyLibraryTheme(characterSides) {
|
|
console.log('Applying library theme with random images...');
|
|
|
|
try {
|
|
// Get images from user's linked directories (same way as lib-image-gallery)
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
console.log('📁 Linked image directories:', linkedDirs.length, 'directories');
|
|
console.log('📁 Directories:', linkedDirs);
|
|
console.log('📁 electronAPI available:', !!window.electronAPI);
|
|
console.log('📁 electronAPI.readDirectory available:', !!(window.electronAPI && window.electronAPI.readDirectory));
|
|
let allImages = [];
|
|
|
|
if (window.electronAPI && linkedDirs.length > 0) {
|
|
console.log('📁 Loading images from directories...');
|
|
const imageExtensions = /\.(jpg|jpeg|png|gif|webp)$/i;
|
|
|
|
// Load images from all linked directories
|
|
for (const dir of linkedDirs) {
|
|
try {
|
|
let files = [];
|
|
if (window.electronAPI.readDirectory) {
|
|
const filesPromise = window.electronAPI.readDirectory(dir.path);
|
|
|
|
if (filesPromise && typeof filesPromise.then === 'function') {
|
|
files = await filesPromise;
|
|
} else if (Array.isArray(filesPromise)) {
|
|
files = filesPromise;
|
|
}
|
|
|
|
if (files && files.length > 0) {
|
|
const imageFiles = files.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return imageExtensions.test(fileName);
|
|
});
|
|
|
|
const dirImages = imageFiles.map(file => {
|
|
if (typeof file === 'object' && file.name && file.path) {
|
|
return { path: file.path, name: file.name };
|
|
} else {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
const fullPath = window.electronAPI.pathJoin ?
|
|
window.electronAPI.pathJoin(dir.path, fileName) :
|
|
`${dir.path}\\${fileName}`;
|
|
return { path: fullPath, name: fileName };
|
|
}
|
|
});
|
|
|
|
allImages = allImages.concat(dirImages);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Error loading images from ${dir.path}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` Found ${allImages.length} images in user library`);
|
|
|
|
if (allImages.length === 0) {
|
|
// No images available - show placeholder or fall back to hentai theme
|
|
console.log('No library images found, falling back to hentai theme');
|
|
localStorage.removeItem('userLibraryImages');
|
|
characterSides.forEach((element, index) => {
|
|
element.style.display = 'block';
|
|
// Fallback to hentai theme
|
|
const imageNumbers = [1, 11, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17];
|
|
const imageNumber = imageNumbers[index];
|
|
if (imageNumber) {
|
|
const imagePath = `assets/hentai/${imageNumber}.png`;
|
|
element.style.backgroundImage = `url('${imagePath}')`;
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Save library images to localStorage for other pages to use
|
|
localStorage.setItem('userLibraryImages', JSON.stringify(allImages));
|
|
|
|
// Dynamic layout variables
|
|
const FIXED_WIDTH = 150; // Fixed width for all images
|
|
const MIN_HEIGHT = 100; // Minimum height
|
|
const MAX_HEIGHT = 300; // Maximum height
|
|
const MARGIN = 0; // No gap between images
|
|
|
|
// Separate left and right columns
|
|
const leftElements = [];
|
|
const rightElements = [];
|
|
|
|
characterSides.forEach((element, index) => {
|
|
if (element.classList.contains('left')) {
|
|
leftElements.push(element);
|
|
} else if (element.classList.contains('right')) {
|
|
rightElements.push(element);
|
|
}
|
|
});
|
|
|
|
// Function to load and position images dynamically
|
|
const loadColumnImages = async (elements, isLeft) => {
|
|
let currentTop = 0;
|
|
const usedImages = new Set(); // Track used images to prevent duplicates
|
|
|
|
for (let i = 0; i < elements.length; i++) {
|
|
const element = elements[i];
|
|
|
|
// Get a random unused image
|
|
let randomImage;
|
|
let attempts = 0;
|
|
do {
|
|
const randomIndex = Math.floor(Math.random() * allImages.length);
|
|
randomImage = allImages[randomIndex];
|
|
attempts++;
|
|
} while (usedImages.has(randomImage.path) && attempts < allImages.length * 2);
|
|
|
|
if (randomImage) {
|
|
usedImages.add(randomImage.path); // Mark as used
|
|
// Properly encode the file path for CSS url()
|
|
let imageUrl;
|
|
if (window.electronAPI) {
|
|
const normalizedPath = randomImage.path.replace(/\\/g, '/');
|
|
imageUrl = `file:///${encodeURI(normalizedPath)}`;
|
|
} else {
|
|
const cleanPath = randomImage.path.replace(/\\/g, '/');
|
|
imageUrl = `file:///${encodeURI(cleanPath)}`;
|
|
}
|
|
|
|
// Load image to get dimensions
|
|
const img = new Image();
|
|
await new Promise((resolve) => {
|
|
img.onload = () => {
|
|
// Calculate height based on aspect ratio
|
|
const aspectRatio = img.height / img.width;
|
|
let calculatedHeight = FIXED_WIDTH * aspectRatio;
|
|
|
|
// Clamp height to min/max bounds
|
|
calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, calculatedHeight));
|
|
|
|
// Apply styles to element
|
|
element.style.display = 'block';
|
|
element.style.width = `${FIXED_WIDTH}px`;
|
|
element.style.height = `${calculatedHeight}px`;
|
|
element.style.top = `${currentTop}px`;
|
|
element.style.backgroundImage = `url("${imageUrl}")`;
|
|
element.style.backgroundSize = 'cover';
|
|
element.style.backgroundPosition = 'center';
|
|
|
|
// Position left or right
|
|
if (isLeft) {
|
|
element.style.left = '0px';
|
|
element.style.right = 'auto';
|
|
} else {
|
|
element.style.right = '0px';
|
|
element.style.left = 'auto';
|
|
}
|
|
|
|
// Update currentTop for next image (small gap)
|
|
currentTop += calculatedHeight + MARGIN;
|
|
|
|
console.log(` ${isLeft ? 'Left' : 'Right'} Panel ${i + 1}: ${randomImage.name} (${FIXED_WIDTH}x${calculatedHeight}px)`);
|
|
resolve();
|
|
};
|
|
|
|
img.onerror = () => {
|
|
console.warn(`Failed to load image: ${randomImage.path}`);
|
|
// Use default height on error
|
|
element.style.display = 'block';
|
|
element.style.width = `${FIXED_WIDTH}px`;
|
|
element.style.height = `${MIN_HEIGHT}px`;
|
|
element.style.top = `${currentTop}px`;
|
|
|
|
if (isLeft) {
|
|
element.style.left = '0px';
|
|
element.style.right = 'auto';
|
|
} else {
|
|
element.style.right = '0px';
|
|
element.style.left = 'auto';
|
|
}
|
|
|
|
currentTop += MIN_HEIGHT + MARGIN;
|
|
resolve();
|
|
};
|
|
|
|
img.src = imageUrl;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// Load both columns in parallel
|
|
await Promise.all([
|
|
loadColumnImages(leftElements, true),
|
|
loadColumnImages(rightElements, false)
|
|
]);
|
|
|
|
console.log(' Library theme applied with dynamic sizing');
|
|
|
|
} catch (error) {
|
|
console.error('Error applying library theme:', error);
|
|
// Fallback to hentai theme on error
|
|
characterSides.forEach((element, index) => {
|
|
element.style.display = 'block';
|
|
const imageNumbers = [1, 11, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17];
|
|
const imageNumber = imageNumbers[index];
|
|
if (imageNumber) {
|
|
const imagePath = `assets/hentai/${imageNumber}.png`;
|
|
element.style.backgroundImage = `url('${imagePath}')`;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize backup system
|
|
function initializeBackupSystem() {
|
|
if (typeof window.BackupManager !== 'undefined') {
|
|
window.backupManager = new window.BackupManager();
|
|
|
|
// Setup backup button handlers
|
|
setupBackupHandlers();
|
|
|
|
// Create initial backup with retry logic
|
|
try {
|
|
window.backupManager.createBackup(false);
|
|
console.log('? Initial backup created on startup');
|
|
} catch (error) {
|
|
console.warn(' Initial backup failed, attempting cleanup and retry:', error);
|
|
try {
|
|
// Perform emergency cleanup and try minimal backup
|
|
window.backupManager.performEmergencyCleanup();
|
|
const minimalBackup = window.backupManager.createMinimalBackup(false);
|
|
const backupKey = `${window.backupManager.backupPrefix}${Date.now()}`;
|
|
localStorage.setItem(backupKey, JSON.stringify(minimalBackup));
|
|
console.log('? Minimal backup created after cleanup');
|
|
} catch (retryError) {
|
|
console.error('? Even minimal backup failed:', retryError);
|
|
}
|
|
}
|
|
} else {
|
|
console.warn(' BackupManager not available');
|
|
}
|
|
}
|
|
|
|
function setupBackupHandlers() {
|
|
// Create manual backup
|
|
const createBackupBtn = document.getElementById('create-backup-btn');
|
|
if (createBackupBtn) {
|
|
createBackupBtn.addEventListener('click', async () => {
|
|
try {
|
|
createBackupBtn.disabled = true;
|
|
createBackupBtn.textContent = ' Creating...';
|
|
|
|
const backupKey = window.backupManager.createBackup(false);
|
|
alert(`? Backup created successfully!\n\nBackup ID: ${backupKey}\n\nThis backup contains all your settings, linked directories, custom content, and player data.`);
|
|
} catch (error) {
|
|
alert(`? Backup creation failed:\n${error.message}`);
|
|
} finally {
|
|
createBackupBtn.disabled = false;
|
|
createBackupBtn.textContent = ' Create Backup';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Restore backup
|
|
const restoreBackupBtn = document.getElementById('restore-backup-btn');
|
|
if (restoreBackupBtn) {
|
|
restoreBackupBtn.addEventListener('click', () => {
|
|
showBackupRestoreDialog();
|
|
});
|
|
}
|
|
|
|
// Export backup
|
|
const exportBackupBtn = document.getElementById('export-backup-btn');
|
|
if (exportBackupBtn) {
|
|
exportBackupBtn.addEventListener('click', () => {
|
|
showBackupExportDialog();
|
|
});
|
|
}
|
|
|
|
// Import backup
|
|
const importBackupBtn = document.getElementById('import-backup-btn');
|
|
if (importBackupBtn) {
|
|
importBackupBtn.addEventListener('click', () => {
|
|
showBackupImportDialog();
|
|
});
|
|
}
|
|
|
|
// Backup info
|
|
const backupInfoBtn = document.getElementById('backup-info-btn');
|
|
if (backupInfoBtn) {
|
|
backupInfoBtn.addEventListener('click', () => {
|
|
showBackupInfo();
|
|
});
|
|
}
|
|
}
|
|
|
|
function showBackupRestoreDialog() {
|
|
const backups = window.backupManager.listBackups();
|
|
|
|
if (backups.length === 0) {
|
|
alert('No backups available to restore.');
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<div class="backup-dialog">
|
|
<h3> Restore Backup</h3>
|
|
<p>Select a backup to restore. This will replace all current data!</p>
|
|
<div class="backup-list">
|
|
`;
|
|
|
|
backups.forEach(backup => {
|
|
const date = new Date(backup.timestamp).toLocaleString();
|
|
const type = backup.isAutomatic ? ' Auto' : ' Manual';
|
|
const size = window.backupManager.formatBytes(backup.size);
|
|
|
|
html += `
|
|
<div class="backup-item" data-key="${backup.key}">
|
|
<div class="backup-header">
|
|
<strong>${type}</strong> - ${date}
|
|
</div>
|
|
<div class="backup-details">
|
|
${backup.totalItems} items � ${size}
|
|
</div>
|
|
<button onclick="restoreSpecificBackup('${backup.key}', '${date}')" class="btn btn-warning btn-small">
|
|
Restore This Backup
|
|
</button>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</div>
|
|
<button onclick="closeBackupDialog()" class="btn btn-secondary">Cancel</button>
|
|
</div>
|
|
`;
|
|
|
|
showModal(html);
|
|
}
|
|
|
|
function showBackupExportDialog() {
|
|
const backups = window.backupManager.listBackups();
|
|
|
|
if (backups.length === 0) {
|
|
alert('No backups available to export.');
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<div class="backup-dialog">
|
|
<h3> Export Backup</h3>
|
|
<p>Select a backup to export to file:</p>
|
|
<div class="backup-list">
|
|
`;
|
|
|
|
backups.forEach(backup => {
|
|
const date = new Date(backup.timestamp).toLocaleString();
|
|
const type = backup.isAutomatic ? ' Auto' : ' Manual';
|
|
const size = window.backupManager.formatBytes(backup.size);
|
|
|
|
html += `
|
|
<div class="backup-item" data-key="${backup.key}">
|
|
<div class="backup-header">
|
|
<strong>${type}</strong> - ${date}
|
|
</div>
|
|
<div class="backup-details">
|
|
${backup.totalItems} items � ${size}
|
|
</div>
|
|
<button onclick="exportSpecificBackup('${backup.key}')" class="btn btn-info btn-small">
|
|
Export This Backup
|
|
</button>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</div>
|
|
<button onclick="closeBackupDialog()" class="btn btn-secondary">Cancel</button>
|
|
</div>
|
|
`;
|
|
|
|
showModal(html);
|
|
}
|
|
|
|
function showBackupImportDialog() {
|
|
const html = `
|
|
<div class="backup-dialog">
|
|
<h3> Import Backup</h3>
|
|
<p>Select a backup file (.json) to import:</p>
|
|
<div class="import-area">
|
|
<input type="file" id="backup-file-input" accept=".json" />
|
|
<button onclick="processBackupImport()" class="btn btn-success">Import Backup</button>
|
|
</div>
|
|
<div class="import-note">
|
|
<small> This will add the backup to your backup list. Use the restore function to apply it.</small>
|
|
</div>
|
|
<button onclick="closeBackupDialog()" class="btn btn-secondary">Cancel</button>
|
|
</div>
|
|
`;
|
|
|
|
showModal(html);
|
|
}
|
|
|
|
function showBackupInfo() {
|
|
const info = window.backupManager.getBackupStorageInfo();
|
|
|
|
let html = `
|
|
<div class="backup-info-dialog">
|
|
<h3>? Backup System Status</h3>
|
|
<div class="backup-stats">
|
|
<div class="stat-row">
|
|
<span>Total Backups:</span>
|
|
<span>${info.totalBackups}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Automatic Backups:</span>
|
|
<span>${info.autoBackups}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Manual Backups:</span>
|
|
<span>${info.manualBackups}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Storage Used:</span>
|
|
<span>${info.formattedSize}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Auto-backup Frequency:</span>
|
|
<span>Every 30 minutes</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Max Auto-backups:</span>
|
|
<span>5 (oldest auto-deleted)</span>
|
|
</div>
|
|
</div>
|
|
<div class="backup-features">
|
|
<h4> Features</h4>
|
|
<ul>
|
|
<li>? Automatic backups every 30 minutes</li>
|
|
<li>? Manual backup creation</li>
|
|
<li>? Selective restore options</li>
|
|
<li>? Export backups to files</li>
|
|
<li>? Import backups from files</li>
|
|
<li>? Emergency backup before risky operations</li>
|
|
</ul>
|
|
</div>
|
|
<button onclick="closeBackupDialog()" class="btn btn-primary">Close</button>
|
|
</div>
|
|
`;
|
|
|
|
showModal(html);
|
|
}
|
|
|
|
window.restoreSpecificBackup = async function(backupKey, date) {
|
|
if (!confirm(` This will replace ALL current data with the backup from:\n\n${date}\n\nAre you sure you want to continue?\n\nCurrent data will be backed up first as a safety measure.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = window.backupManager.restoreBackup(backupKey);
|
|
closeBackupDialog();
|
|
alert(`? Backup restored successfully!\n\n${result.restoredItems} items restored from ${result.backupTimestamp}\n\nSafety backup created: ${result.restorePointKey}\n\nPlease restart the application to fully apply changes.`);
|
|
} catch (error) {
|
|
alert(`? Backup restore failed:\n${error.message}`);
|
|
}
|
|
};
|
|
|
|
window.exportSpecificBackup = async function(backupKey) {
|
|
try {
|
|
const result = await window.backupManager.exportBackup(backupKey);
|
|
closeBackupDialog();
|
|
alert(`? Backup exported successfully!\n\nFile: ${result.filename}\n\nYou can now store this file safely and import it later if needed.`);
|
|
} catch (error) {
|
|
alert(`? Backup export failed:\n${error.message}`);
|
|
}
|
|
};
|
|
|
|
window.processBackupImport = async function() {
|
|
const fileInput = document.getElementById('backup-file-input');
|
|
const file = fileInput.files[0];
|
|
|
|
if (!file) {
|
|
alert('Please select a backup file first.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await window.backupManager.importBackup(file);
|
|
closeBackupDialog();
|
|
alert(`? Backup imported successfully!\n\nBackup from: ${result.timestamp}\nItems: ${result.totalItems}\n\nThe backup is now available in the restore list.`);
|
|
} catch (error) {
|
|
alert(`? Backup import failed:\n${error.message}`);
|
|
}
|
|
};
|
|
|
|
function showModal(content) {
|
|
// Remove existing modal if any
|
|
const existingModal = document.querySelector('.backup-modal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'backup-modal';
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 10000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
|
|
const modalContent = document.createElement('div');
|
|
modalContent.style.cssText = `
|
|
background: var(--bg-secondary);
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
color: white;
|
|
`;
|
|
modalContent.innerHTML = content;
|
|
|
|
modal.appendChild(modalContent);
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
window.closeBackupDialog = function() {
|
|
const modal = document.querySelector('.backup-modal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
};
|
|
|
|
// Initialize theme dropdown functionality
|
|
async function initializeThemeDropdown() {
|
|
const themeDropdown = document.getElementById('theme-dropdown');
|
|
if (themeDropdown) {
|
|
// Load saved theme preference
|
|
const savedTheme = localStorage.getItem('selectedTheme') || 'hentai';
|
|
themeDropdown.value = savedTheme;
|
|
|
|
// Apply the saved theme (refreshes library theme with new random images)
|
|
await applyTheme(savedTheme);
|
|
|
|
// Handle theme changes
|
|
themeDropdown.addEventListener('change', async (e) => {
|
|
await applyTheme(e.target.value);
|
|
});
|
|
}
|
|
|
|
// Initialize color theme dropdown
|
|
const colorThemeDropdown = document.getElementById('color-theme-dropdown');
|
|
if (colorThemeDropdown && window.themeManager) {
|
|
// Load current theme
|
|
const currentTheme = window.themeManager.getCurrentTheme();
|
|
colorThemeDropdown.value = currentTheme;
|
|
|
|
// Handle color theme changes
|
|
colorThemeDropdown.addEventListener('change', (e) => {
|
|
window.themeManager.applyTheme(e.target.value);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Game Guide Toggle
|
|
function initializeGameGuide() {
|
|
const guideBtn = document.getElementById('game-guide-btn');
|
|
const guidePanel = document.getElementById('game-guide');
|
|
const guideBackdrop = document.getElementById('game-guide-backdrop');
|
|
const guideClose = document.getElementById('game-guide-close');
|
|
|
|
console.log(' Initializing game guide...', {
|
|
guideBtn: !!guideBtn,
|
|
guidePanel: !!guidePanel,
|
|
guideBackdrop: !!guideBackdrop,
|
|
guideClose: !!guideClose
|
|
});
|
|
|
|
if (guideBtn && guidePanel && guideBackdrop) {
|
|
// Ensure both start hidden using classList
|
|
guidePanel.classList.remove('popup-visible');
|
|
guideBackdrop.classList.remove('popup-visible');
|
|
|
|
guideBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Game guide button clicked!');
|
|
const isVisible = guidePanel.classList.contains('popup-visible');
|
|
console.log(' Current visibility:', isVisible);
|
|
|
|
if (isVisible) {
|
|
guidePanel.classList.remove('popup-visible');
|
|
guideBackdrop.classList.remove('popup-visible');
|
|
// Force clear inline styles
|
|
guidePanel.style.display = '';
|
|
guideBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
} else {
|
|
// Clear any inline styles before adding class
|
|
guidePanel.style.display = '';
|
|
guideBackdrop.style.display = '';
|
|
guidePanel.classList.add('popup-visible');
|
|
guideBackdrop.classList.add('popup-visible');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
console.log(' After toggle - visible:', guidePanel.classList.contains('popup-visible'));
|
|
});
|
|
|
|
// Close on backdrop click
|
|
guideBackdrop.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Backdrop clicked');
|
|
guidePanel.classList.remove('popup-visible');
|
|
guideBackdrop.classList.remove('popup-visible');
|
|
// Force clear inline styles
|
|
guidePanel.style.display = '';
|
|
guideBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
});
|
|
|
|
// Close on X button click
|
|
if (guideClose) {
|
|
guideClose.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Close button clicked');
|
|
guidePanel.classList.remove('popup-visible');
|
|
guideBackdrop.classList.remove('popup-visible');
|
|
// Force clear inline styles
|
|
guidePanel.style.display = '';
|
|
guideBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
});
|
|
}
|
|
|
|
console.log('? Game guide initialized successfully');
|
|
} else {
|
|
console.error('? Game guide initialization failed - missing elements');
|
|
}
|
|
}
|
|
|
|
// Options Menu Toggle
|
|
function initializeOptionsMenu() {
|
|
const optionsBtn = document.getElementById('options-menu-btn');
|
|
const optionsMenu = document.getElementById('options-menu');
|
|
const optionsBackdrop = document.getElementById('options-backdrop');
|
|
const optionsClose = document.getElementById('options-close');
|
|
|
|
console.log(' Initializing options menu...', {
|
|
optionsBtn: !!optionsBtn,
|
|
optionsMenu: !!optionsMenu,
|
|
optionsBackdrop: !!optionsBackdrop,
|
|
optionsClose: !!optionsClose
|
|
});
|
|
|
|
if (!optionsBtn) {
|
|
console.error('? Options button not found!');
|
|
return;
|
|
}
|
|
if (!optionsMenu) {
|
|
console.error('? Options menu not found!');
|
|
return;
|
|
}
|
|
if (!optionsBackdrop) {
|
|
console.error('? Options backdrop not found!');
|
|
return;
|
|
}
|
|
|
|
if (optionsBtn && optionsMenu && optionsBackdrop) {
|
|
console.log('? Adding click listener to options button');
|
|
|
|
// Ensure both start hidden using classList
|
|
optionsMenu.classList.remove('popup-visible');
|
|
optionsBackdrop.classList.remove('popup-visible');
|
|
|
|
optionsBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Options button clicked!');
|
|
|
|
const isVisible = optionsMenu.classList.contains('popup-visible');
|
|
console.log('Current visibility:', isVisible);
|
|
|
|
if (isVisible) {
|
|
optionsMenu.classList.remove('popup-visible');
|
|
optionsBackdrop.classList.remove('popup-visible');
|
|
document.body.style.overflow = '';
|
|
} else {
|
|
optionsMenu.classList.add('popup-visible');
|
|
optionsBackdrop.classList.add('popup-visible');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
console.log('After toggle - visible:', optionsMenu.classList.contains('popup-visible'));
|
|
});
|
|
console.log('? Options button click listener attached');
|
|
|
|
// Close on backdrop click
|
|
optionsBackdrop.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Options backdrop clicked - closing popup');
|
|
optionsMenu.classList.remove('popup-visible');
|
|
optionsBackdrop.classList.remove('popup-visible');
|
|
// Force clear inline styles
|
|
optionsMenu.style.display = '';
|
|
optionsBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
console.log(' Options closed via backdrop');
|
|
});
|
|
|
|
// Close on X button click
|
|
if (optionsClose) {
|
|
optionsClose.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(' Options close button clicked');
|
|
console.log(' Before remove - classes:', optionsMenu.className);
|
|
console.log(' Before remove - inline style:', optionsMenu.style.display);
|
|
optionsMenu.classList.remove('popup-visible');
|
|
optionsBackdrop.classList.remove('popup-visible');
|
|
// Force clear inline styles
|
|
optionsMenu.style.display = '';
|
|
optionsBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
console.log(' After remove - classes:', optionsMenu.className);
|
|
console.log(' After remove - inline style:', optionsMenu.style.display);
|
|
console.log(' Menu has popup-visible?', optionsMenu.classList.contains('popup-visible'));
|
|
console.log(' Options closed via button');
|
|
});
|
|
}
|
|
|
|
console.log('? Options menu initialized successfully');
|
|
} else {
|
|
console.error('? Options menu initialization failed - missing elements');
|
|
}
|
|
}
|
|
|
|
// Load profile data into header
|
|
function loadHeaderProfile() {
|
|
// Get username from userProfile object
|
|
let username = 'Player';
|
|
const userProfile = localStorage.getItem('userProfile');
|
|
if (userProfile) {
|
|
try {
|
|
const profile = JSON.parse(userProfile);
|
|
username = profile.username || 'Player';
|
|
} catch (e) {
|
|
console.error('Error parsing user profile:', e);
|
|
}
|
|
}
|
|
|
|
const profilePicture = localStorage.getItem('profilePicture');
|
|
|
|
const usernameElement = document.getElementById('header-username');
|
|
const avatarContainer = document.getElementById('header-profile-avatar');
|
|
|
|
if (usernameElement) {
|
|
usernameElement.textContent = username;
|
|
}
|
|
|
|
if (avatarContainer && profilePicture) {
|
|
avatarContainer.innerHTML = `<img src="${profilePicture}" alt="Profile">`;
|
|
}
|
|
|
|
// Make profile display clickable to open profile page
|
|
const profileDisplay = document.querySelector('.user-profile-display');
|
|
if (profileDisplay) {
|
|
profileDisplay.addEventListener('click', () => {
|
|
window.location.href = 'user-profile.html';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize application when page loads
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
console.log(' DOM Content Loaded - Initializing UI components...');
|
|
|
|
// Load profile data
|
|
loadHeaderProfile();
|
|
|
|
// Initialize options menu early
|
|
initializeOptionsMenu();
|
|
|
|
// Initialize game guide
|
|
initializeGameGuide();
|
|
|
|
// Shared Escape key handler for all popups
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
const optionsMenu = document.getElementById('options-menu');
|
|
const optionsBackdrop = document.getElementById('options-backdrop');
|
|
const guidePanel = document.getElementById('game-guide');
|
|
const guideBackdrop = document.getElementById('game-guide-backdrop');
|
|
|
|
if (optionsMenu?.classList.contains('popup-visible')) {
|
|
optionsMenu.classList.remove('popup-visible');
|
|
optionsBackdrop.classList.remove('popup-visible');
|
|
optionsMenu.style.display = '';
|
|
optionsBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
if (guidePanel?.classList.contains('popup-visible')) {
|
|
guidePanel.classList.remove('popup-visible');
|
|
guideBackdrop.classList.remove('popup-visible');
|
|
guidePanel.style.display = '';
|
|
guideBackdrop.style.display = '';
|
|
document.body.style.overflow = '';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
window.addEventListener('load', () => {
|
|
// Initialize theme dropdown
|
|
initializeThemeDropdown();
|
|
|
|
setTimeout(() => {
|
|
if (window.forceFixGame) {
|
|
window.forceFixGame();
|
|
}
|
|
}, 2000);
|
|
});
|
|
|
|
// Video management UI handlers
|
|
let videoPlayerManager = null;
|
|
|
|
// Initialize video player when available
|
|
function initializeVideoPlayer() {
|
|
if (window.VideoPlayerManager && !videoPlayerManager) {
|
|
videoPlayerManager = new VideoPlayerManager();
|
|
window.videoPlayerManager = videoPlayerManager; // Expose globally
|
|
console.log('? Video player initialized');
|
|
}
|
|
}
|
|
|
|
// Video management screen handlers
|
|
let videoHandlersAttached = false;
|
|
|
|
function setupVideoManagementHandlers() {
|
|
// Prevent multiple attachments
|
|
if (videoHandlersAttached) {
|
|
console.log('Video handlers already attached, skipping...');
|
|
return;
|
|
}
|
|
|
|
console.log('Setting up video management handlers...');
|
|
videoHandlersAttached = true;
|
|
// Back button
|
|
const backBtn = document.getElementById('back-to-start-from-video-btn');
|
|
if (backBtn) {
|
|
backBtn.addEventListener('click', () => {
|
|
if (window.game && typeof window.game.showScreen === 'function') {
|
|
window.game.showScreen('start-screen');
|
|
}
|
|
});
|
|
}
|
|
|
|
function showVideoStorageInfo() {
|
|
const categories = ['background', 'task', 'reward', 'punishment'];
|
|
let totalVideos = 0;
|
|
let totalSize = 0;
|
|
let invalidCount = 0;
|
|
|
|
const details = categories.map(category => {
|
|
const videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
|
const validVideos = videos.filter(v => !v.url.startsWith('blob:'));
|
|
const invalid = videos.length - validVideos.length;
|
|
const categorySize = validVideos.reduce((sum, v) => sum + (v.size || 0), 0);
|
|
|
|
totalVideos += validVideos.length;
|
|
totalSize += categorySize;
|
|
invalidCount += invalid;
|
|
|
|
return `${category}: ${validVideos.length} videos (${formatFileSize(categorySize)})${invalid > 0 ? ` + ${invalid} invalid` : ''}`;
|
|
}).join('\n');
|
|
|
|
// Calculate current localStorage usage
|
|
const currentStorageSize = new Blob([JSON.stringify(localStorage)]).size;
|
|
const storageLimit = 5 * 1024 * 1024; // 5MB conservative estimate
|
|
const usagePercent = Math.round((currentStorageSize / storageLimit) * 100);
|
|
|
|
const storageWarning = currentStorageSize > storageLimit * 0.8 ?
|
|
'\n Warning: Storage nearly full!' : '';
|
|
|
|
alert(`Video Storage Info:\n\n${details}\n\nTotal: ${totalVideos} videos (${formatFileSize(totalSize)})${invalidCount > 0 ? `\nInvalid entries: ${invalidCount}` : ''}\n\nLocalStorage Usage: ${formatFileSize(currentStorageSize)} / ~${formatFileSize(storageLimit)} (${usagePercent}%)${storageWarning}\n\nTip: Use videos under 10MB for best performance.`);
|
|
}
|
|
|
|
function manualCleanupInvalidVideos() {
|
|
const categories = ['background', 'task', 'reward', 'punishment'];
|
|
let totalCleaned = 0;
|
|
|
|
categories.forEach(category => {
|
|
const videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
|
const validVideos = videos.filter(video => !video.url.startsWith('blob:'));
|
|
const cleaned = videos.length - validVideos.length;
|
|
|
|
if (cleaned > 0) {
|
|
localStorage.setItem(`${category}-videos`, JSON.stringify(validVideos));
|
|
totalCleaned += cleaned;
|
|
// Refresh the gallery if this category is currently active
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
}
|
|
});
|
|
|
|
if (totalCleaned > 0) {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(` Cleaned up ${totalCleaned} invalid video entries`, 'success');
|
|
}
|
|
// Refresh current gallery
|
|
const activeTab = document.querySelector('.video-tabs .tab-btn.active');
|
|
if (activeTab) {
|
|
const category = activeTab.id.replace('-video-tab', '');
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
}
|
|
} else {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('No invalid videos found to clean up', 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearAllVideos() {
|
|
const categories = ['background', 'task', 'reward', 'punishment'];
|
|
let totalCleared = 0;
|
|
|
|
categories.forEach(category => {
|
|
const videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
|
totalCleared += videos.length;
|
|
localStorage.removeItem(`${category}-videos`);
|
|
|
|
// Refresh the gallery if this category is currently active
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
});
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? Cleared ${totalCleared} videos and freed up storage space`, 'success');
|
|
}
|
|
|
|
// Refresh current gallery
|
|
const activeTab = document.querySelector('.video-tabs .tab-btn.active');
|
|
if (activeTab) {
|
|
const category = activeTab.id.replace('-video-tab', '');
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
}
|
|
}
|
|
|
|
// Directory management buttons
|
|
const addDirectoryBtn = document.getElementById('add-video-directory-btn');
|
|
if (addDirectoryBtn) {
|
|
addDirectoryBtn.addEventListener('click', () => {
|
|
handleAddVideoDirectory();
|
|
});
|
|
}
|
|
|
|
const refreshAllBtn = document.getElementById('refresh-all-directories-btn');
|
|
if (refreshAllBtn) {
|
|
refreshAllBtn.addEventListener('click', () => {
|
|
handleRefreshAllDirectories();
|
|
});
|
|
}
|
|
|
|
const clearAllDirectoriesBtn = document.getElementById('clear-all-directories-btn');
|
|
if (clearAllDirectoriesBtn) {
|
|
clearAllDirectoriesBtn.addEventListener('click', () => {
|
|
handleClearAllDirectories();
|
|
});
|
|
}
|
|
|
|
// [REMOVED] Gallery control buttons - lib-video-gallery uses direct preview instead of selection system
|
|
|
|
// Refresh videos button
|
|
const refreshVideosBtn = document.getElementById('refresh-videos-btn');
|
|
if (refreshVideosBtn) {
|
|
refreshVideosBtn.addEventListener('click', () => {
|
|
refreshVideoLibrary();
|
|
});
|
|
}
|
|
|
|
// Preview close button
|
|
const closePreviewBtn = document.getElementById('close-video-preview-btn');
|
|
if (closePreviewBtn) {
|
|
closePreviewBtn.addEventListener('click', () => {
|
|
closeVideoPreview();
|
|
});
|
|
}
|
|
|
|
// Video loop toggle button
|
|
const toggleLoopBtn = document.getElementById('toggle-video-loop-btn');
|
|
if (toggleLoopBtn) {
|
|
toggleLoopBtn.addEventListener('click', () => {
|
|
toggleVideoLoop();
|
|
});
|
|
}
|
|
|
|
// Test buttons
|
|
const testBackgroundBtn = document.getElementById('test-background-video');
|
|
if (testBackgroundBtn) {
|
|
testBackgroundBtn.addEventListener('click', () => {
|
|
testVideoPlayback('background');
|
|
});
|
|
}
|
|
|
|
const testOverlayBtn = document.getElementById('test-overlay-video');
|
|
if (testOverlayBtn) {
|
|
testOverlayBtn.addEventListener('click', () => {
|
|
testVideoPlayback('overlay');
|
|
});
|
|
}
|
|
|
|
const stopAllBtn = document.getElementById('stop-all-videos');
|
|
if (stopAllBtn) {
|
|
stopAllBtn.addEventListener('click', () => {
|
|
stopAllVideos();
|
|
});
|
|
}
|
|
|
|
// Storage info button
|
|
const storageInfoBtn = document.getElementById('video-storage-info-btn');
|
|
if (storageInfoBtn) {
|
|
storageInfoBtn.addEventListener('click', () => {
|
|
showVideoStorageInfo();
|
|
});
|
|
}
|
|
|
|
// Cleanup button
|
|
const cleanupBtn = document.getElementById('cleanup-invalid-videos-btn');
|
|
if (cleanupBtn) {
|
|
cleanupBtn.addEventListener('click', () => {
|
|
if (confirm('Clean up all invalid video entries? This will remove broken video references but keep valid videos.')) {
|
|
manualCleanupInvalidVideos();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clear all videos button
|
|
const clearAllBtn = document.getElementById('clear-all-videos-btn');
|
|
if (clearAllBtn) {
|
|
clearAllBtn.addEventListener('click', () => {
|
|
if (confirm(' Delete ALL videos from ALL categories? This cannot be undone!')) {
|
|
clearAllVideos();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Settings handlers
|
|
setupVideoSettingsHandlers();
|
|
|
|
// Initialize the unified video system
|
|
if (window.electronAPI && window.desktopFileManager) {
|
|
console.log(' Initializing unified video library...');
|
|
|
|
// The file manager will automatically load linked directories
|
|
// No need to scan old category directories anymore
|
|
setTimeout(() => {
|
|
updateDirectoryList();
|
|
updateVideoStats();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
}, 500);
|
|
} else {
|
|
// Browser fallback - video gallery loading moved to lib-video-gallery system
|
|
}
|
|
|
|
// Clean up any existing invalid blob URLs from previous sessions
|
|
cleanupInvalidVideoData();
|
|
}
|
|
|
|
function cleanupInvalidVideoData() {
|
|
const categories = ['background', 'task', 'reward', 'punishment'];
|
|
let totalCleaned = 0;
|
|
|
|
categories.forEach(category => {
|
|
const videos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
|
const validVideos = videos.filter(video => {
|
|
if (video.url && video.url.startsWith('blob:')) {
|
|
totalCleaned++;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (validVideos.length !== videos.length) {
|
|
localStorage.setItem(`${category}-videos`, JSON.stringify(validVideos));
|
|
}
|
|
});
|
|
|
|
if (totalCleaned > 0) {
|
|
console.log(` Cleaned up ${totalCleaned} invalid blob URL videos from localStorage`);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(` Cleaned up ${totalCleaned} invalid video entries`, 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
|
|
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();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
}).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();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(' 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: var(--text-dim); 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: var(--bg-tertiary); border-radius: 4px;">
|
|
<div>
|
|
<strong>${dir.name}</strong>
|
|
<br><small style="color: var(--text-muted);">${dir.path}</small>
|
|
<br><small style="color: var(--text-dim);">${dir.videoCount} videos</small>
|
|
</div>
|
|
<button class="btn btn-small btn-outline" onclick="removeDirectory(${dir.id})" style="color: var(--color-error);">Remove</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function removeDirectory(directoryId) {
|
|
if (window.electronAPI && window.desktopFileManager) {
|
|
window.desktopFileManager.removeVideoDirectory(directoryId).then((success) => {
|
|
if (success) {
|
|
updateDirectoryList();
|
|
updateVideoStats();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
// loadUnifiedVideoGallery() function removed - using lib-video-gallery instead
|
|
|
|
// loadStandardVideoGallery() function removed - using lib-video-gallery instead
|
|
|
|
// loadLargeVideoGallery() function removed - using lib-video-gallery instead
|
|
|
|
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: var(--text-dim);"> ${video.directoryName || 'Unknown'}</small>
|
|
${video.size ? `<small style="color: var(--text-dim);"> � ${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: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; color: var(--text-dim);">
|
|
<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: var(--text-dim);"> ${video.directoryName || 'Unknown'}</small>
|
|
${video.size ? `<small style="color: var(--text-dim);"> � ${formatFileSize(video.size)}</small>` : ''}
|
|
</div>
|
|
<button class="btn btn-small" onclick="previewVideo('${video.url}', '${video.title}')" style="margin-top: 8px;background: var(--color-primary) !important;color: #ffffff !important;border: 1px solid var(--color-primary) !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);
|
|
});
|
|
}
|
|
|
|
// [REMOVED] setupVideoItemHandlers function - part of unified video gallery cleanup
|
|
|
|
// 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();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
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) {
|
|
console.log(`Importing ${category} videos...`);
|
|
|
|
if (window.electronAPI && window.desktopFileManager) {
|
|
// Desktop version - use desktop file manager with native file dialog
|
|
window.desktopFileManager.selectAndImportVideos(category).then((importedVideos) => {
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
if (window.game && window.game.showNotification && importedVideos.length > 0) {
|
|
window.game.showNotification(`? ${importedVideos.length} ${category} videos imported successfully!`, 'success');
|
|
}
|
|
}).catch(error => {
|
|
console.error('Video import failed:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Video import failed', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
// Web version - use file input and process files
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'video/*';
|
|
input.multiple = true;
|
|
input.multiple = true;
|
|
|
|
input.onchange = async (e) => {
|
|
const files = Array.from(e.target.files);
|
|
console.log(`Selected ${files.length} ${category} video files`);
|
|
|
|
if (files.length === 0) return;
|
|
|
|
// Show progress notification
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`Processing ${files.length} video files...`, 'info');
|
|
}
|
|
|
|
// Process each file
|
|
let processedCount = 0;
|
|
const videoList = [];
|
|
|
|
for (const file of files) {
|
|
try {
|
|
// Check file size (limit to 50MB for localStorage compatibility)
|
|
const maxSize = 50 * 1024 * 1024; // 50MB (will become ~67MB as base64)
|
|
if (file.size > maxSize) {
|
|
console.warn(` Skipping ${file.name}: File too large (${formatFileSize(file.size)}). Maximum size: 50MB`);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(` ${file.name} is too large (max 50MB)`, 'warning');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Check available localStorage space
|
|
const currentStorageSize = new Blob([JSON.stringify(localStorage)]).size;
|
|
const estimatedVideoSize = file.size * 1.37; // Base64 overhead estimate
|
|
const storageLimit = 5 * 1024 * 1024; // Conservative 5MB limit
|
|
|
|
if (currentStorageSize + estimatedVideoSize > storageLimit) {
|
|
console.warn(` Skipping ${file.name}: Would exceed storage quota. Current: ${formatFileSize(currentStorageSize)}, Video: ${formatFileSize(estimatedVideoSize)}`);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(` Storage quota exceeded. Try smaller videos or clear some data.`, 'error');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
console.log(`Processing video: ${file.name} (${formatFileSize(file.size)})`);
|
|
|
|
// Convert file to data URL for reliable storage and playback
|
|
const videoDataURL = await fileToDataURL(file);
|
|
|
|
const videoData = {
|
|
name: file.name,
|
|
url: videoDataURL,
|
|
size: file.size,
|
|
type: file.type,
|
|
category: category,
|
|
addedDate: new Date().toISOString()
|
|
};
|
|
|
|
videoList.push(videoData);
|
|
processedCount++;
|
|
|
|
console.log(`? Processed ${category} video: ${file.name}`);
|
|
} catch (error) {
|
|
console.error(`? Failed to process video ${file.name}:`, error);
|
|
}
|
|
}
|
|
|
|
if (processedCount > 0) {
|
|
try {
|
|
// Save videos to localStorage
|
|
const existingVideos = JSON.parse(localStorage.getItem(`${category}-videos`) || '[]');
|
|
const updatedVideos = [...existingVideos, ...videoList];
|
|
|
|
// Try to save and handle quota exceeded error
|
|
localStorage.setItem(`${category}-videos`, JSON.stringify(updatedVideos));
|
|
|
|
// Refresh the gallery
|
|
// [REMOVED] loadVideoGalleryContent call - part of unified video gallery cleanup
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? ${processedCount} ${category} videos uploaded successfully!`, 'success');
|
|
}
|
|
} catch (error) {
|
|
if (error.name === 'QuotaExceededError') {
|
|
console.error('? LocalStorage quota exceeded:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Storage quota exceeded. Try smaller videos or clear browser data.', 'error');
|
|
}
|
|
} else {
|
|
console.error('? Failed to save videos:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to save videos', 'error');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
}
|
|
}
|
|
|
|
// [REMOVED] loadVideoGalleryContent function - part of unified video gallery cleanup
|
|
|
|
function formatFileSize(bytes) {
|
|
if (!bytes || 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(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function fileToDataURL(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
console.log(`? Converted ${file.name} to data URL`);
|
|
resolve(e.target.result);
|
|
};
|
|
reader.onerror = (e) => {
|
|
console.error(`? Failed to read ${file.name}:`, e);
|
|
reject(e);
|
|
};
|
|
reader.onprogress = (e) => {
|
|
if (e.lengthComputable) {
|
|
const progress = Math.round((e.loaded / e.total) * 100);
|
|
if (progress % 25 === 0) { // Log every 25%
|
|
console.log(` Reading ${file.name}: ${progress}%`);
|
|
}
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function previewVideo(videoUrl, videoName) {
|
|
const previewSection = document.getElementById('video-preview-section');
|
|
const previewPlayer = document.getElementById('video-preview-player');
|
|
const previewNameElement = document.getElementById('preview-video-name');
|
|
const toggleLoopBtn = document.getElementById('toggle-video-loop-btn');
|
|
|
|
if (previewSection && previewPlayer && previewNameElement) {
|
|
// Stop any currently playing video
|
|
previewPlayer.pause();
|
|
previewPlayer.currentTime = 0;
|
|
|
|
// Reset loop state for new video
|
|
previewPlayer.loop = false;
|
|
if (toggleLoopBtn) {
|
|
toggleLoopBtn.textContent = ' Loop';
|
|
toggleLoopBtn.classList.remove('btn-active');
|
|
}
|
|
|
|
// Set new video source
|
|
previewPlayer.src = videoUrl;
|
|
previewNameElement.textContent = videoName;
|
|
previewSection.style.display = 'block';
|
|
previewSection.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
// Autoplay the video
|
|
previewPlayer.play().catch(error => {
|
|
console.warn('Autoplay failed:', error);
|
|
});
|
|
|
|
console.log(` Previewing video: ${videoName}`);
|
|
}
|
|
}
|
|
|
|
// [REMOVED] deleteVideo function - part of unified video gallery cleanup, was only used by removed category galleries
|
|
|
|
// [REMOVED] selectAllVideos and deselectAllVideos functions - lib-video-gallery doesn't use checkbox selection
|
|
|
|
// [REMOVED] deleteSelectedVideos function - lib-video-gallery doesn't use checkbox selection system
|
|
|
|
function refreshVideoLibrary() {
|
|
if (window.electronAPI && window.desktopFileManager) {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(' Refreshing video library...', 'info');
|
|
}
|
|
|
|
// Use the new unified system - refresh all linked directories
|
|
window.desktopFileManager.refreshAllDirectories().then(() => {
|
|
// Update UI
|
|
updateDirectoryList();
|
|
updateVideoStats();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
const videoCount = window.desktopFileManager.getAllVideos().length;
|
|
window.game.showNotification(`? Video library refreshed! Found ${videoCount} videos`, 'success');
|
|
}
|
|
}).catch(error => {
|
|
console.error('Error refreshing video library:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to refresh video library', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Refresh is only available in desktop mode', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeVideoPreview() {
|
|
const previewSection = document.getElementById('video-preview-section');
|
|
const previewPlayer = document.getElementById('video-preview-player');
|
|
|
|
if (previewSection && previewPlayer) {
|
|
// Stop playback and clear source
|
|
previewPlayer.pause();
|
|
previewPlayer.currentTime = 0;
|
|
previewPlayer.src = '';
|
|
|
|
previewSection.style.display = 'none';
|
|
console.log(' Video preview closed');
|
|
}
|
|
}
|
|
|
|
function toggleVideoLoop() {
|
|
const previewPlayer = document.getElementById('video-preview-player');
|
|
const toggleLoopBtn = document.getElementById('toggle-video-loop-btn');
|
|
|
|
if (previewPlayer && toggleLoopBtn) {
|
|
// Toggle the loop attribute
|
|
previewPlayer.loop = !previewPlayer.loop;
|
|
|
|
// Update button appearance and text
|
|
if (previewPlayer.loop) {
|
|
toggleLoopBtn.textContent = ' Loop ON';
|
|
toggleLoopBtn.classList.add('btn-active');
|
|
console.log(' Video loop enabled');
|
|
} else {
|
|
toggleLoopBtn.textContent = ' Loop';
|
|
toggleLoopBtn.classList.remove('btn-active');
|
|
console.log(' Video loop disabled');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function testVideoPlayback(type) {
|
|
console.log(`Testing ${type} video playback`);
|
|
|
|
if (type === 'overlay') {
|
|
try {
|
|
// Show overlay video player with random video
|
|
const overlayPlayer = await OverlayVideoPlayer.showOverlay({
|
|
showQuality: false,
|
|
showSpeed: false,
|
|
minimal: false
|
|
});
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(' Overlay video player opened!', 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error showing overlay video player:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Error opening overlay video player', 'error');
|
|
}
|
|
}
|
|
} else {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`${type} video test - feature coming soon!`, 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopAllVideos() {
|
|
console.log('Stopping all video playback');
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('All videos stopped', 'info');
|
|
}
|
|
}
|
|
|
|
function setupVideoSettingsHandlers() {
|
|
const settings = [
|
|
'enable-video-player',
|
|
'enable-background-videos',
|
|
'enable-task-videos',
|
|
'video-autoplay',
|
|
'video-show-controls',
|
|
'video-fade-transitions'
|
|
];
|
|
|
|
settings.forEach(settingId => {
|
|
const element = document.getElementById(settingId);
|
|
if (element && element.type === 'checkbox') {
|
|
element.addEventListener('change', () => {
|
|
localStorage.setItem(settingId, element.checked);
|
|
console.log(`Video setting ${settingId}: ${element.checked}`);
|
|
});
|
|
|
|
// Load saved setting
|
|
const saved = localStorage.getItem(settingId);
|
|
if (saved !== null) {
|
|
element.checked = saved === 'true';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Volume slider
|
|
const volumeSlider = document.getElementById('video-volume');
|
|
const volumeDisplay = document.getElementById('video-volume-display');
|
|
if (volumeSlider && volumeDisplay) {
|
|
volumeSlider.addEventListener('input', () => {
|
|
const volume = volumeSlider.value;
|
|
volumeDisplay.textContent = `${volume}%`;
|
|
localStorage.setItem('video-volume', volume);
|
|
});
|
|
|
|
// Load saved volume
|
|
const savedVolume = localStorage.getItem('video-volume') || '30';
|
|
volumeSlider.value = savedVolume;
|
|
volumeDisplay.textContent = `${savedVolume}%`;
|
|
}
|
|
}
|
|
|
|
// ===== LEVEL CALCULATION SYSTEM =====
|
|
// Exponential XP scaling: starts at 10 XP for level 2, then grows exponentially
|
|
// Formula: XP = Math.floor(10 * Math.pow(1.5, level - 2)) for levels 2+
|
|
const levelData = [
|
|
{ level: 1, name: "Virgin", xpRequired: 0 },
|
|
{ level: 2, name: "Curious", xpRequired: 10 }, // 10 XP
|
|
{ level: 3, name: "Eager", xpRequired: 25 }, // 15 more (25 total)
|
|
{ level: 4, name: "Aroused", xpRequired: 48 }, // 23 more (48 total)
|
|
{ level: 5, name: "Lustful", xpRequired: 82 }, // 34 more (82 total)
|
|
{ level: 6, name: "Passionate", xpRequired: 133 }, // 51 more (133 total)
|
|
{ level: 7, name: "Addicted", xpRequired: 209 }, // 76 more (209 total)
|
|
{ level: 8, name: "Obsessed", xpRequired: 323 }, // 114 more (323 total)
|
|
{ level: 9, name: "Deviant", xpRequired: 494 }, // 171 more (494 total)
|
|
{ level: 10, name: "Kinky", xpRequired: 751 }, // 257 more (751 total)
|
|
{ level: 11, name: "Perverted", xpRequired: 1137 }, // 386 more (1137 total)
|
|
{ level: 12, name: "Depraved", xpRequired: 1716 }, // 579 more (1716 total)
|
|
{ level: 13, name: "Dominant", xpRequired: 2585 }, // 869 more (2585 total)
|
|
{ level: 14, name: "Submissive", xpRequired: 3889 }, // 1304 more (3889 total)
|
|
{ level: 15, name: "Hedonist", xpRequired: 5844 }, // 1955 more (5844 total)
|
|
{ level: 16, name: "Insatiable", xpRequired: 8777 }, // 2933 more (8777 total)
|
|
{ level: 17, name: "Transcendent", xpRequired: 13176 }, // 4399 more (13176 total)
|
|
{ level: 18, name: "Enlightened", xpRequired: 19775 }, // 6599 more (19775 total)
|
|
{ level: 19, name: "Godlike", xpRequired: 29673 }, // 9898 more (29673 total)
|
|
{ level: 20, name: "Omnipotent", xpRequired: 44520 } // 14847 more (44520 total)
|
|
];
|
|
|
|
function calculateLevel(totalXP) {
|
|
// Find the highest level that the user has reached
|
|
let currentLevel = levelData[0];
|
|
for (let i = levelData.length - 1; i >= 0; i--) {
|
|
if (totalXP >= levelData[i].xpRequired) {
|
|
currentLevel = levelData[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate progress toward next level
|
|
const nextLevel = levelData.find(l => l.level === currentLevel.level + 1);
|
|
const currentLevelXP = totalXP - currentLevel.xpRequired;
|
|
const xpNeededForNext = nextLevel ? nextLevel.xpRequired - currentLevel.xpRequired : 100;
|
|
const progressPercent = nextLevel ? (currentLevelXP / xpNeededForNext) * 100 : 100;
|
|
|
|
return {
|
|
level: currentLevel.level,
|
|
name: currentLevel.name,
|
|
totalXP: totalXP,
|
|
currentLevelXP: currentLevelXP,
|
|
xpNeededForNext: xpNeededForNext,
|
|
progressPercent: Math.min(progressPercent, 100),
|
|
nextLevelName: nextLevel ? nextLevel.name : 'Max Level'
|
|
};
|
|
}
|
|
|
|
function updateLevelDisplay() {
|
|
// Get total XP from PlayerStats if available
|
|
let totalXP = 0;
|
|
if (window.playerStats && window.playerStats.stats) {
|
|
totalXP = window.playerStats.stats.totalXP || 0;
|
|
} else {
|
|
// Fallback to localStorage
|
|
const savedStats = localStorage.getItem('playerStats');
|
|
if (savedStats) {
|
|
try {
|
|
const stats = JSON.parse(savedStats);
|
|
totalXP = stats.totalXP || 0;
|
|
} catch (e) {
|
|
console.warn('Could not parse saved stats for level display');
|
|
}
|
|
}
|
|
}
|
|
|
|
const levelInfo = calculateLevel(totalXP);
|
|
|
|
// Update the display elements
|
|
const levelNameEl = document.getElementById('current-level-name');
|
|
const levelNumberEl = document.getElementById('current-level-number');
|
|
const levelXPEl = document.getElementById('level-xp-header');
|
|
const progressEl = document.getElementById('level-xp-progress');
|
|
const progressCurrentEl = document.getElementById('level-progress-current');
|
|
const progressNextEl = document.getElementById('level-progress-next');
|
|
|
|
if (levelNameEl) levelNameEl.textContent = levelInfo.name;
|
|
if (levelNumberEl) levelNumberEl.textContent = levelInfo.level;
|
|
if (levelXPEl) levelXPEl.textContent = levelInfo.totalXP.toLocaleString();
|
|
if (progressEl) progressEl.style.width = levelInfo.progressPercent + '%';
|
|
if (progressCurrentEl) progressCurrentEl.textContent = levelInfo.currentLevelXP;
|
|
if (progressNextEl) progressNextEl.textContent = levelInfo.xpNeededForNext;
|
|
|
|
console.log(` Level Display Updated: ${levelInfo.name} (Level ${levelInfo.level}) - ${levelInfo.totalXP} XP`);
|
|
}
|
|
|
|
// Update level display when page loads and periodically
|
|
function initializeLevelDisplay() {
|
|
updateLevelDisplay();
|
|
|
|
// Update every 5 seconds to catch XP changes
|
|
setInterval(updateLevelDisplay, 5000);
|
|
|
|
// Also update when PlayerStats changes (if available)
|
|
if (window.addEventListener) {
|
|
window.addEventListener('playerStatsUpdated', updateLevelDisplay);
|
|
|
|
// Update when page gains focus (returning from other pages)
|
|
window.addEventListener('focus', () => {
|
|
console.log(' Page gained focus, refreshing XP display');
|
|
setTimeout(updateLevelDisplay, 500); // Small delay to ensure any stats updates are saved
|
|
});
|
|
|
|
// Update when page becomes visible (tab switching)
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden) {
|
|
console.log(' Page became visible, refreshing XP display');
|
|
setTimeout(updateLevelDisplay, 500);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== VIDEO BILLBOARD FUNCTIONALITY =====
|
|
let billboardVideoSystem = {
|
|
videos: [],
|
|
currentVideoIndex: 0,
|
|
isPlaying: true,
|
|
isMuted: true,
|
|
playTimeout: null,
|
|
|
|
async initialize() {
|
|
console.log(' Initializing Video Billboard...');
|
|
|
|
// Try to load videos directly using the same method as Quick Play
|
|
await this.loadVideosDirectly();
|
|
|
|
// If no videos found, try fallback methods
|
|
if (this.videos.length === 0) {
|
|
await this.tryFallbackVideos();
|
|
}
|
|
|
|
if (this.videos.length === 0) {
|
|
console.warn(' No videos available for billboard');
|
|
return;
|
|
}
|
|
|
|
console.log(` Billboard loaded ${this.videos.length} videos`);
|
|
this.setupVideoElement();
|
|
this.setupControls();
|
|
this.startVideoRotation();
|
|
},
|
|
|
|
async loadVideosDirectly() {
|
|
try {
|
|
// Use the same video loading logic as Quick Play
|
|
console.log(' Loading videos using Quick Play method...');
|
|
|
|
// Get linked directories from localStorage (same as Quick Play)
|
|
let linkedDirs;
|
|
try {
|
|
linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedDirs)) {
|
|
linkedDirs = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedVideoDirectories, resetting to empty array:', e);
|
|
linkedDirs = [];
|
|
}
|
|
|
|
let linkedIndividualVideos;
|
|
try {
|
|
linkedIndividualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]');
|
|
if (!Array.isArray(linkedIndividualVideos)) {
|
|
linkedIndividualVideos = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedIndividualVideos, resetting to empty array:', e);
|
|
linkedIndividualVideos = [];
|
|
}
|
|
|
|
console.log(` Found ${linkedDirs.length} linked directories, ${linkedIndividualVideos.length} individual videos`);
|
|
|
|
// Load videos from linked directories using Electron API (same as Quick Play)
|
|
if (window.electronAPI && linkedDirs.length > 0) {
|
|
const videoExtensions = /\.(mp4|webm|avi|mov|mkv|wmv|flv|m4v)$/i;
|
|
|
|
for (const dir of linkedDirs) {
|
|
try {
|
|
console.log(` Scanning directory: ${dir.path}`);
|
|
|
|
// Use video-specific directory reading for better results
|
|
let files = [];
|
|
if (window.electronAPI.readVideoDirectory) {
|
|
files = await window.electronAPI.readVideoDirectory(dir.path);
|
|
} else if (window.electronAPI.readVideoDirectoryRecursive) {
|
|
files = await window.electronAPI.readVideoDirectoryRecursive(dir.path);
|
|
} else if (window.electronAPI.readDirectory) {
|
|
const allFiles = await window.electronAPI.readDirectory(dir.path);
|
|
files = allFiles.filter(file => videoExtensions.test(file.name));
|
|
}
|
|
|
|
if (files && files.length > 0) {
|
|
console.log(` Found ${files.length} video files in ${dir.path}`);
|
|
files.forEach(file => {
|
|
this.videos.push({
|
|
name: file.name,
|
|
path: file.path,
|
|
filePath: file.path,
|
|
size: file.size || 0,
|
|
duration: file.duration || 0,
|
|
directory: dir.path
|
|
});
|
|
});
|
|
} else {
|
|
console.log(` No video files found in ${dir.path}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`? Error loading videos from directory ${dir.path}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add individual linked video files
|
|
linkedIndividualVideos.forEach(video => {
|
|
this.videos.push({
|
|
name: video.name || 'Unknown Video',
|
|
path: video.path,
|
|
filePath: video.path,
|
|
size: video.size || 0,
|
|
directory: 'individual'
|
|
});
|
|
});
|
|
|
|
console.log(` Billboard total videos loaded: ${this.videos.length}`);
|
|
|
|
} catch (error) {
|
|
console.error('? Error loading videos directly:', error);
|
|
}
|
|
},
|
|
|
|
async tryFallbackVideos() {
|
|
// Try the same fallback methods as Quick Play
|
|
console.log(' Trying fallback video discovery methods...');
|
|
|
|
// Try unified storage (Quick Play method)
|
|
const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
|
|
let allVideos = unifiedData.allVideos || [];
|
|
console.log(` Got ${allVideos.length} videos from unified storage`);
|
|
|
|
// Try legacy storage (Quick Play method)
|
|
if (allVideos.length === 0) {
|
|
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
|
allVideos = Object.values(storedVideos).flat();
|
|
console.log(` Got ${allVideos.length} videos from legacy storage`);
|
|
}
|
|
|
|
// Try VideoLibrary localStorage (Quick Play method)
|
|
if (allVideos.length === 0) {
|
|
const videoLibraryData = JSON.parse(localStorage.getItem('videoLibrary') || '[]');
|
|
allVideos = Array.isArray(videoLibraryData) ? videoLibraryData : [];
|
|
console.log(` Got ${allVideos.length} videos from VideoLibrary localStorage`);
|
|
}
|
|
|
|
// Try to access the global VideoLibrary if available
|
|
if (allVideos.length === 0 && window.VideoLibrary && window.VideoLibrary.videos) {
|
|
allVideos = window.VideoLibrary.videos;
|
|
console.log(` Got ${allVideos.length} videos from global VideoLibrary.videos`);
|
|
}
|
|
|
|
// Try desktop file manager
|
|
if (allVideos.length === 0 && window.desktopFileManager) {
|
|
try {
|
|
if (typeof window.desktopFileManager.getAllVideos === 'function') {
|
|
allVideos = window.desktopFileManager.getAllVideos();
|
|
console.log(` Got ${allVideos.length} videos from DesktopFileManager`);
|
|
}
|
|
} catch (dmError) {
|
|
console.warn(' Error getting videos from DesktopFileManager:', dmError);
|
|
}
|
|
}
|
|
|
|
if (allVideos.length > 0) {
|
|
this.videos = allVideos.slice(0, 50); // Limit to first 50 for performance
|
|
console.log(` Loaded ${this.videos.length} videos from fallback methods`);
|
|
return;
|
|
}
|
|
|
|
console.log(' No video sources found, creating demo placeholder');
|
|
this.createDemoVideo();
|
|
},
|
|
|
|
async getVideosFromLibrarySystem() {
|
|
try {
|
|
// Try to access the global video library data that's being loaded
|
|
if (window.game && window.game.fileManager && window.game.fileManager.allLinkedVideos) {
|
|
const linkedVideos = window.game.fileManager.allLinkedVideos;
|
|
if (linkedVideos && linkedVideos.length > 0) {
|
|
console.log(` Found ${linkedVideos.length} videos from game fileManager`);
|
|
return linkedVideos;
|
|
}
|
|
}
|
|
|
|
// Try desktop file manager
|
|
if (window.desktopFileManager && window.desktopFileManager.allLinkedVideos) {
|
|
const linkedVideos = window.desktopFileManager.allLinkedVideos;
|
|
if (linkedVideos && linkedVideos.length > 0) {
|
|
console.log(` Found ${linkedVideos.length} videos from desktop fileManager`);
|
|
return linkedVideos;
|
|
}
|
|
}
|
|
|
|
// Try to check if we can find any active video data being processed
|
|
const libraryTab = document.querySelector('#lib-video-gallery');
|
|
if (libraryTab && libraryTab.children.length > 0) {
|
|
console.log(' Found videos in library gallery, attempting to extract data...');
|
|
return this.extractVideosFromGallery();
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.warn(' Error accessing library system videos:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
extractVideosFromGallery() {
|
|
const videos = [];
|
|
const galleryItems = document.querySelectorAll('#lib-video-gallery .video-item');
|
|
|
|
galleryItems.forEach((item, index) => {
|
|
const videoElement = item.querySelector('video');
|
|
const titleElement = item.querySelector('.video-title');
|
|
|
|
if (videoElement && videoElement.src) {
|
|
let src = videoElement.src;
|
|
// Remove the file:// prefix if present
|
|
if (src.startsWith('file://')) {
|
|
src = src.substring(7);
|
|
}
|
|
|
|
videos.push({
|
|
name: titleElement ? titleElement.textContent : `Video ${index + 1}`,
|
|
path: src,
|
|
filePath: src
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log(` Extracted ${videos.length} videos from gallery`);
|
|
return videos;
|
|
},
|
|
|
|
createDemoVideo() {
|
|
// Create a demo placeholder that shows the billboard is working
|
|
const demoContainer = document.querySelector('.billboard-frame');
|
|
if (demoContainer) {
|
|
const video = document.getElementById('billboard-video');
|
|
if (video) {
|
|
video.style.display = 'none';
|
|
}
|
|
|
|
// Create demo content
|
|
const demoDiv = document.createElement('div');
|
|
demoDiv.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: var(--color-background-gradient);
|
|
background-size: 400% 400%;
|
|
animation: gradientShift 3s ease infinite;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-family: 'Audiowide', monospace;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
z-index: 1;
|
|
`;
|
|
|
|
demoDiv.innerHTML = `
|
|
<div>
|
|
<div style="font-size: 18px; margin-bottom: 10px;"></div>
|
|
<div>CYBERPUNK BILLBOARD</div>
|
|
<div style="font-size: 10px; margin-top: 5px; opacity: 0.7;">Awaiting Video Library</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add animation keyframes if not already present
|
|
if (!document.querySelector('#billboard-demo-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'billboard-demo-styles';
|
|
style.textContent = `
|
|
@keyframes gradientShift {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
demoContainer.appendChild(demoDiv);
|
|
console.log(' Created demo billboard content');
|
|
}
|
|
},
|
|
|
|
setupVideoElement() {
|
|
const video = document.getElementById('billboard-video');
|
|
if (!video) return;
|
|
|
|
// Set performance optimizations (like Quick Play)
|
|
video.muted = this.isMuted;
|
|
video.volume = 0.3;
|
|
video.playsInline = true;
|
|
video.preload = 'metadata';
|
|
|
|
// Add performance CSS properties via JavaScript
|
|
video.style.backfaceVisibility = 'hidden';
|
|
video.style.transform = 'translateZ(0)';
|
|
video.style.willChange = 'opacity';
|
|
|
|
// Set up event listeners for smooth playback
|
|
video.addEventListener('loadstart', () => {
|
|
console.log(' Billboard video loading started');
|
|
});
|
|
|
|
video.addEventListener('loadedmetadata', () => {
|
|
console.log(' Billboard metadata loaded');
|
|
});
|
|
|
|
video.addEventListener('canplay', () => {
|
|
console.log(' Billboard video ready to play');
|
|
});
|
|
|
|
video.addEventListener('error', (e) => {
|
|
console.error('? Billboard video error:', e);
|
|
this.loadRandomVideo();
|
|
});
|
|
|
|
video.addEventListener('ended', () => {
|
|
// Immediately load next video when current one ends
|
|
this.loadRandomVideo();
|
|
});
|
|
|
|
// Handle stalled playback
|
|
video.addEventListener('stalled', () => {
|
|
console.warn(' Billboard video stalled, reloading...');
|
|
setTimeout(() => this.loadRandomVideo(), 1000);
|
|
});
|
|
|
|
// Handle waiting/buffering
|
|
video.addEventListener('waiting', () => {
|
|
console.log(' Billboard video buffering...');
|
|
});
|
|
|
|
console.log(' Billboard video element configured for smooth playback');
|
|
},
|
|
|
|
setupControls() {
|
|
const muteBtn = document.getElementById('billboard-mute');
|
|
const pauseBtn = document.getElementById('billboard-pause');
|
|
|
|
if (muteBtn) {
|
|
muteBtn.addEventListener('click', () => this.toggleMute());
|
|
}
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', () => this.togglePause());
|
|
}
|
|
|
|
this.updateControlButtons();
|
|
},
|
|
|
|
updateControlButtons() {
|
|
const muteBtn = document.getElementById('billboard-mute');
|
|
const pauseBtn = document.getElementById('billboard-pause');
|
|
|
|
if (muteBtn) {
|
|
muteBtn.textContent = this.isMuted ? '' : '';
|
|
muteBtn.title = this.isMuted ? 'Unmute' : 'Mute';
|
|
}
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.textContent = this.isPlaying ? '' : '';
|
|
pauseBtn.title = this.isPlaying ? 'Pause' : 'Play';
|
|
}
|
|
},
|
|
|
|
async startVideoRotation() {
|
|
if (this.videos.length === 0) return;
|
|
|
|
this.loadRandomVideo();
|
|
},
|
|
|
|
async loadRandomVideo() {
|
|
if (this.videos.length === 0) return;
|
|
|
|
// Clear any existing timeout
|
|
if (this.playTimeout) {
|
|
clearTimeout(this.playTimeout);
|
|
}
|
|
|
|
// Select random video
|
|
this.currentVideoIndex = Math.floor(Math.random() * this.videos.length);
|
|
const selectedVideo = this.videos[this.currentVideoIndex];
|
|
|
|
const video = document.getElementById('billboard-video');
|
|
const source = document.getElementById('billboard-video-source');
|
|
if (!video || !source || !selectedVideo) return;
|
|
|
|
try {
|
|
// Fade out current video smoothly
|
|
video.style.opacity = '0';
|
|
|
|
// Wait for fade out, then load new video
|
|
setTimeout(() => {
|
|
const videoPath = selectedVideo.path || selectedVideo.filePath;
|
|
if (videoPath) {
|
|
console.log(` Billboard loading: ${selectedVideo.name || selectedVideo.title || 'Unknown'}`);
|
|
|
|
// Use source element for better loading (like Quick Play)
|
|
source.src = `file://${videoPath}`;
|
|
video.load(); // Force reload with new source
|
|
|
|
// Handle metadata loaded event
|
|
video.onloadedmetadata = () => {
|
|
// Set random start time for variety
|
|
const duration = video.duration;
|
|
if (duration > 10) {
|
|
const maxStart = Math.max(0, duration - 10); // Ensure we have at least 10 seconds to play
|
|
const randomStart = Math.random() * maxStart;
|
|
video.currentTime = randomStart;
|
|
}
|
|
};
|
|
|
|
// Handle when video can start playing
|
|
video.oncanplay = () => {
|
|
// Fade in smoothly
|
|
video.style.opacity = '1';
|
|
|
|
if (this.isPlaying) {
|
|
// Use a small delay to ensure smooth playback
|
|
setTimeout(() => {
|
|
video.play().catch(e => {
|
|
console.warn('Billboard autoplay prevented:', e);
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
// Set timeout for next video (8 seconds)
|
|
this.playTimeout = setTimeout(() => {
|
|
this.loadRandomVideo();
|
|
}, 8000);
|
|
};
|
|
|
|
// Handle loading errors
|
|
video.onerror = () => {
|
|
console.warn('Billboard video loading error, trying next video');
|
|
setTimeout(() => this.loadRandomVideo(), 1000);
|
|
};
|
|
}
|
|
}, 500); // Wait for fade out
|
|
|
|
} catch (error) {
|
|
console.error('? Error loading billboard video:', error);
|
|
// Try next video after a delay
|
|
setTimeout(() => this.loadRandomVideo(), 2000);
|
|
}
|
|
},
|
|
|
|
toggleMute() {
|
|
const video = document.getElementById('billboard-video');
|
|
if (!video) return;
|
|
|
|
this.isMuted = !this.isMuted;
|
|
video.muted = this.isMuted;
|
|
this.updateControlButtons();
|
|
|
|
console.log(` Billboard ${this.isMuted ? 'muted' : 'unmuted'}`);
|
|
},
|
|
|
|
togglePause() {
|
|
const video = document.getElementById('billboard-video');
|
|
if (!video) return;
|
|
|
|
this.isPlaying = !this.isPlaying;
|
|
|
|
if (this.isPlaying) {
|
|
video.play().catch(e => console.warn('Billboard play prevented:', e));
|
|
// Resume rotation
|
|
if (!this.playTimeout) {
|
|
this.playTimeout = setTimeout(() => {
|
|
this.loadRandomVideo();
|
|
}, 8000);
|
|
}
|
|
} else {
|
|
video.pause();
|
|
// Stop rotation
|
|
if (this.playTimeout) {
|
|
clearTimeout(this.playTimeout);
|
|
this.playTimeout = null;
|
|
}
|
|
}
|
|
|
|
this.updateControlButtons();
|
|
console.log(` Billboard ${this.isPlaying ? 'resumed' : 'paused'}`);
|
|
},
|
|
|
|
// Method to refresh videos when they become available
|
|
async refreshVideos() {
|
|
console.log(' Refreshing billboard videos...');
|
|
const videos = await this.getVideosFromLibrarySystem();
|
|
if (videos && videos.length > 0) {
|
|
this.videos = videos.slice(0, 50);
|
|
console.log(` Billboard refreshed with ${this.videos.length} videos`);
|
|
|
|
// Remove demo content if it exists
|
|
const demoContent = document.querySelector('.billboard-frame > div');
|
|
if (demoContent && demoContent.innerHTML.includes('CYBERPUNK BILLBOARD')) {
|
|
demoContent.remove();
|
|
const video = document.getElementById('billboard-video');
|
|
if (video) {
|
|
video.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Start playing if we weren't before
|
|
if (this.videos.length > 0) {
|
|
this.startVideoRotation();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Global function to refresh billboard when videos become available
|
|
window.refreshBillboard = function() {
|
|
if (billboardVideoSystem) {
|
|
billboardVideoSystem.refreshVideos();
|
|
}
|
|
};
|
|
|
|
function initializeBillboardVideo() {
|
|
// Wait a bit for other systems to load, then try multiple times
|
|
let attempts = 0;
|
|
const maxAttempts = 5;
|
|
|
|
const tryInitialize = async () => {
|
|
attempts++;
|
|
console.log(` Billboard initialization attempt ${attempts}/${maxAttempts}`);
|
|
|
|
await billboardVideoSystem.initialize();
|
|
|
|
// If we still have no videos and haven't reached max attempts, try again
|
|
if (billboardVideoSystem.videos.length === 0 && attempts < maxAttempts) {
|
|
console.log(` No videos found on attempt ${attempts}, retrying in 3 seconds...`);
|
|
setTimeout(tryInitialize, 3000);
|
|
} else if (billboardVideoSystem.videos.length > 0) {
|
|
console.log(` Billboard successfully initialized with ${billboardVideoSystem.videos.length} videos`);
|
|
} else {
|
|
console.log(' Billboard initialized with demo content after all attempts');
|
|
}
|
|
};
|
|
|
|
// Start the first attempt after a delay
|
|
setTimeout(tryInitialize, 5000);
|
|
}
|
|
|
|
// Initialize everything when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setTimeout(() => {
|
|
// Initialize PlayerStats first
|
|
if (typeof PlayerStats !== 'undefined' && !window.playerStats) {
|
|
window.playerStats = new PlayerStats();
|
|
console.log(' PlayerStats initialized on home page');
|
|
}
|
|
|
|
initializeVideoPlayer();
|
|
|
|
// Initialize desktop file manager if in Electron environment
|
|
if (window.electronAPI && typeof DesktopFileManager !== 'undefined') {
|
|
window.desktopFileManager = new DesktopFileManager(window.game?.dataManager);
|
|
console.log('📁 Desktop File Manager initialized for video management');
|
|
|
|
// Clean up legacy localStorage video directories
|
|
const legacyDirs = localStorage.getItem('linkedVideoDirectories');
|
|
const legacyIndividual = localStorage.getItem('linkedIndividualVideos');
|
|
if (legacyDirs || legacyIndividual) {
|
|
console.log('🧹 Cleaning up legacy localStorage video directories...');
|
|
localStorage.removeItem('linkedVideoDirectories');
|
|
localStorage.removeItem('linkedIndividualVideos');
|
|
console.log('✅ Legacy video directory storage cleared');
|
|
}
|
|
|
|
// Initialize the new unified video system
|
|
setTimeout(() => {
|
|
updateDirectoryList();
|
|
updateVideoStats();
|
|
// Video gallery loading moved to lib-video-gallery system
|
|
}, 1000);
|
|
|
|
// Initialize backup system
|
|
initializeBackupSystem();
|
|
|
|
// Set up global localStorage quota handler
|
|
const originalSetItem = localStorage.setItem.bind(localStorage);
|
|
localStorage.setItem = function(key, value) {
|
|
try {
|
|
originalSetItem(key, value);
|
|
} catch (error) {
|
|
if (error.name === 'QuotaExceededError') {
|
|
console.error(' LocalStorage quota exceeded! Performing emergency cleanup...');
|
|
if (window.backupManager) {
|
|
window.backupManager.performEmergencyCleanup();
|
|
}
|
|
// Try again after cleanup
|
|
try {
|
|
originalSetItem(key, value);
|
|
console.log('? Save succeeded after cleanup');
|
|
} catch (retryError) {
|
|
console.error('? Save failed even after cleanup:', retryError);
|
|
alert(' Storage is full! Photo data has been cleared. Please restart the app.');
|
|
}
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
} // Set up video management button (only once)
|
|
const videoManageBtn = document.getElementById('manage-video-btn');
|
|
if (videoManageBtn && !videoManageBtn.hasAttribute('data-handler-attached')) {
|
|
videoManageBtn.setAttribute('data-handler-attached', 'true');
|
|
videoManageBtn.addEventListener('click', () => {
|
|
if (window.game && typeof window.game.showScreen === 'function') {
|
|
window.game.showScreen('video-management-screen');
|
|
// Set up handlers when screen is shown (with delay to ensure DOM is ready)
|
|
setTimeout(() => {
|
|
setupVideoManagementHandlers();
|
|
}, 100);
|
|
} else {
|
|
console.error('Game instance not available for video management');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set up quick play button (only once)
|
|
const quickPlayBtn = document.getElementById('quick-play-btn');
|
|
if (quickPlayBtn && !quickPlayBtn.hasAttribute('data-handler-attached')) {
|
|
quickPlayBtn.setAttribute('data-handler-attached', 'true');
|
|
quickPlayBtn.addEventListener('click', () => {
|
|
console.log('? Opening Quick Play...');
|
|
window.location.href = 'quick-play.html';
|
|
});
|
|
}
|
|
|
|
// Set up campaign mode button (only once)
|
|
const campaignBtn = document.getElementById('campaign-btn');
|
|
if (campaignBtn && !campaignBtn.hasAttribute('data-handler-attached')) {
|
|
campaignBtn.setAttribute('data-handler-attached', 'true');
|
|
campaignBtn.addEventListener('click', () => {
|
|
console.log('🎯 Opening Campaign Mode...');
|
|
window.location.href = 'campaign.html';
|
|
});
|
|
}
|
|
|
|
// Set up training academy button (only once)
|
|
const trainingAcademyBtn = document.getElementById('training-academy-btn');
|
|
if (trainingAcademyBtn && !trainingAcademyBtn.hasAttribute('data-handler-attached')) {
|
|
trainingAcademyBtn.setAttribute('data-handler-attached', 'true');
|
|
trainingAcademyBtn.addEventListener('click', () => {
|
|
console.log(' Opening Training Academy...');
|
|
window.location.href = 'training-academy.html';
|
|
});
|
|
}
|
|
|
|
// Set up porn cinema button (only once)
|
|
const pornCinemaBtn = document.getElementById('porn-cinema-btn');
|
|
if (pornCinemaBtn && !pornCinemaBtn.hasAttribute('data-handler-attached')) {
|
|
pornCinemaBtn.setAttribute('data-handler-attached', 'true');
|
|
pornCinemaBtn.addEventListener('click', () => {
|
|
console.log(' Opening Porn Cinema...');
|
|
window.location.href = 'porn-cinema.html';
|
|
});
|
|
}
|
|
|
|
// Set up hypno gallery button (only once)
|
|
const hypnoGalleryBtn = document.getElementById('hypno-gallery-btn');
|
|
if (hypnoGalleryBtn && !hypnoGalleryBtn.hasAttribute('data-handler-attached')) {
|
|
hypnoGalleryBtn.setAttribute('data-handler-attached', 'true');
|
|
hypnoGalleryBtn.addEventListener('click', () => {
|
|
console.log(' Opening Hypno Gallery...');
|
|
window.location.href = 'hypno-gallery.html';
|
|
});
|
|
}
|
|
|
|
// Set up user profile button (only once)
|
|
const userProfileBtn = document.getElementById('user-profile-btn');
|
|
if (userProfileBtn && !userProfileBtn.hasAttribute('data-handler-attached')) {
|
|
userProfileBtn.setAttribute('data-handler-attached', 'true');
|
|
userProfileBtn.addEventListener('click', () => {
|
|
console.log(' Opening User Profile...');
|
|
window.location.href = 'user-profile.html';
|
|
});
|
|
}
|
|
|
|
// Set up library button (only once)
|
|
const libraryBtn = document.getElementById('library-btn');
|
|
console.log(' Library button found:', !!libraryBtn);
|
|
|
|
if (libraryBtn && !libraryBtn.hasAttribute('data-handler-attached')) {
|
|
console.log(' Attaching library button handler...');
|
|
libraryBtn.setAttribute('data-handler-attached', 'true');
|
|
libraryBtn.addEventListener('click', () => {
|
|
console.log(' Library button clicked - navigating to library.html');
|
|
window.location.href = 'library.html';
|
|
});
|
|
console.log('? Library button handler attached successfully');
|
|
} else if (libraryBtn && libraryBtn.hasAttribute('data-handler-attached')) {
|
|
console.log(' Library button handler already attached');
|
|
} else {
|
|
console.error('? Library button not found in DOM');
|
|
}
|
|
|
|
// Set up back to start from library button
|
|
const backToStartFromLibraryBtn = document.getElementById('back-to-start-from-library-btn');
|
|
if (backToStartFromLibraryBtn && !backToStartFromLibraryBtn.hasAttribute('data-handler-attached')) {
|
|
backToStartFromLibraryBtn.setAttribute('data-handler-attached', 'true');
|
|
backToStartFromLibraryBtn.addEventListener('click', () => {
|
|
if (window.game && typeof window.game.showScreen === 'function') {
|
|
window.game.showScreen('start-screen');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set up clear overall XP button (debug tool)
|
|
const clearXpBtn = document.getElementById('clear-overall-xp-btn');
|
|
if (clearXpBtn && !clearXpBtn.hasAttribute('data-handler-attached')) {
|
|
clearXpBtn.setAttribute('data-handler-attached', 'true');
|
|
clearXpBtn.addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to reset your Overall XP to 0? This cannot be undone.')) {
|
|
if (window.game && window.game.dataManager) {
|
|
window.game.dataManager.set('overallXp', 0);
|
|
window.game.updateOverallXpDisplay(); // Update header display
|
|
alert('Overall XP has been reset to 0!');
|
|
console.log(' Overall XP reset to 0 for testing');
|
|
} else {
|
|
alert('Game not initialized yet. Please try again after the game loads.');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize Video Billboard
|
|
initializeBillboardVideo();
|
|
|
|
// Initialize Level Display
|
|
initializeLevelDisplay();
|
|
|
|
// Set up a listener for when videos are loaded in the library
|
|
setTimeout(() => {
|
|
if (window.refreshBillboard) {
|
|
window.refreshBillboard();
|
|
}
|
|
}, 10000); // Check again after 10 seconds for loaded videos
|
|
}, 1000);
|
|
});
|
|
|
|
// Setup Library Tab Handlers
|
|
function setupLibraryHandlers() {
|
|
console.log('Setting up library handlers...');
|
|
|
|
// Set up library tab switching
|
|
const libraryTabs = document.querySelectorAll('.library-tab');
|
|
const libraryContents = document.querySelectorAll('.library-content');
|
|
|
|
libraryTabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const targetTab = tab.getAttribute('data-tab');
|
|
|
|
// Remove active class from all tabs and contents
|
|
libraryTabs.forEach(t => t.classList.remove('active'));
|
|
libraryContents.forEach(c => c.classList.remove('active'));
|
|
|
|
// Add active class to clicked tab and corresponding content
|
|
tab.classList.add('active');
|
|
const targetContent = document.getElementById(`library-${targetTab}-content`);
|
|
if (targetContent) {
|
|
targetContent.classList.add('active');
|
|
|
|
// Initialize specific functionality for each tab
|
|
switch(targetTab) {
|
|
case 'images':
|
|
setupLibraryImagesTab();
|
|
break;
|
|
case 'audio':
|
|
setupLibraryAudioTab();
|
|
break;
|
|
case 'video':
|
|
setupLibraryVideoTab();
|
|
break;
|
|
case 'gallery':
|
|
setupLibraryGalleryTab();
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log(`Switched to library tab: ${targetTab}`);
|
|
});
|
|
});
|
|
|
|
// Set up gallery category switching for gallery tab
|
|
const galleryCategoryBtns = document.querySelectorAll('.gallery-category-btn');
|
|
const photoGalleries = document.querySelectorAll('.photo-gallery');
|
|
|
|
galleryCategoryBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const targetCategory = btn.getAttribute('data-category');
|
|
|
|
// Remove active class from all category buttons and galleries
|
|
galleryCategoryBtns.forEach(b => b.classList.remove('active'));
|
|
photoGalleries.forEach(g => g.classList.remove('active'));
|
|
|
|
// Add active class to clicked button and corresponding gallery
|
|
btn.classList.add('active');
|
|
const targetGallery = document.getElementById(`lib-${targetCategory}-photos-gallery`);
|
|
if (targetGallery) {
|
|
targetGallery.classList.add('active');
|
|
}
|
|
|
|
console.log(`Switched to gallery category: ${targetCategory}`);
|
|
});
|
|
});
|
|
|
|
// Set up refresh library button
|
|
const refreshLibraryBtn = document.getElementById('refresh-library-btn');
|
|
if (refreshLibraryBtn) {
|
|
refreshLibraryBtn.addEventListener('click', () => {
|
|
console.log('Refreshing library...');
|
|
refreshAllLibraryContent();
|
|
});
|
|
}
|
|
|
|
// Initialize the default tab (images) after a delay to ensure game data is loaded
|
|
setTimeout(() => {
|
|
setupLibraryImagesTab();
|
|
}, 1000);
|
|
|
|
// Listen for game ready event to refresh content
|
|
window.addEventListener('gameReady', () => {
|
|
console.log(' Game ready event received, refreshing library content...');
|
|
refreshAllLibraryContent();
|
|
});
|
|
|
|
console.log('Library handlers setup complete');
|
|
}
|
|
|
|
// Library button click handler setup
|
|
function setupLibraryButtonHandler() {
|
|
const libraryBtn = document.getElementById('library-btn');
|
|
if (libraryBtn) {
|
|
libraryBtn.onclick = function(e) {
|
|
e.preventDefault();
|
|
console.log(' Library button clicked - navigating to library.html');
|
|
window.location.href = 'library.html';
|
|
};
|
|
}
|
|
}
|
|
|
|
// Setup library button handler after page load
|
|
setTimeout(setupLibraryButtonHandler, 2000);
|
|
|
|
// Setup individual library tab functionality
|
|
async function setupLibraryImagesTab(retryCount = 0) {
|
|
console.log('Setting up images tab functionality...');
|
|
|
|
// Wait for game to be available (max 10 retries)
|
|
if (!window.game && retryCount < 10) {
|
|
console.log(`? Game not ready yet, retrying in 500ms... (attempt ${retryCount + 1}/10)`);
|
|
setTimeout(() => setupLibraryImagesTab(retryCount + 1), 500);
|
|
return;
|
|
}
|
|
|
|
if (!window.game) {
|
|
console.error('? Game not available after 10 retries, aborting image tab setup');
|
|
return;
|
|
}
|
|
|
|
// Get current image stats from multiple possible locations
|
|
let taskImages = [];
|
|
let consequenceImages = [];
|
|
|
|
// Try dataManager first (most likely location based on console output)
|
|
if (window.game.dataManager && window.game.dataManager.gameData) {
|
|
taskImages = window.game.dataManager.gameData.taskImages || [];
|
|
consequenceImages = window.game.dataManager.gameData.consequenceImages || [];
|
|
}
|
|
|
|
// Try gameData directly
|
|
if (taskImages.length === 0 && window.game.gameData) {
|
|
taskImages = window.game.gameData.taskImages || [];
|
|
consequenceImages = window.game.gameData.consequenceImages || [];
|
|
}
|
|
|
|
// Try fileManager (desktop file manager)
|
|
if (taskImages.length === 0 && window.game.fileManager) {
|
|
taskImages = window.game.fileManager.taskImages || [];
|
|
consequenceImages = window.game.fileManager.consequenceImages || [];
|
|
|
|
// Try desktop file manager methods if available
|
|
if (taskImages.length === 0 && window.desktopFileManager) {
|
|
console.log(' Trying desktop file manager...');
|
|
try {
|
|
// Desktop file manager might have different method names
|
|
if (window.desktopFileManager.getTaskImages) {
|
|
taskImages = window.desktopFileManager.getTaskImages() || [];
|
|
}
|
|
if (window.desktopFileManager.getConsequenceImages) {
|
|
consequenceImages = window.desktopFileManager.getConsequenceImages() || [];
|
|
}
|
|
console.log(` Desktop file manager found: ${taskImages.length} task, ${consequenceImages.length} consequence images`);
|
|
} catch (e) {
|
|
console.log(' Desktop file manager error:', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try taskImagePaths and consequenceImagePaths
|
|
if (taskImages.length === 0 && window.game.taskImagePaths) {
|
|
taskImages = window.game.taskImagePaths;
|
|
}
|
|
if (consequenceImages.length === 0 && window.game.consequenceImagePaths) {
|
|
consequenceImages = window.game.consequenceImagePaths;
|
|
}
|
|
|
|
// Try other possible locations
|
|
if (taskImages.length === 0) {
|
|
taskImages = window.game.discoveredTaskImages || window.game.taskImages || [];
|
|
|
|
// Try accessing images through the discoverImages function
|
|
if (taskImages.length === 0 && window.game.discoverImages) {
|
|
console.log(' Trying to call discoverImages function...');
|
|
try {
|
|
// The discoverImages function might populate image arrays
|
|
const discovered = window.game.discoverImages();
|
|
if (discovered && discovered.taskImages) {
|
|
taskImages = discovered.taskImages;
|
|
}
|
|
if (discovered && discovered.consequenceImages) {
|
|
consequenceImages = discovered.consequenceImages;
|
|
}
|
|
console.log(` discoverImages function returned: ${taskImages.length} task, ${consequenceImages.length} consequence images`);
|
|
} catch (e) {
|
|
console.log(' discoverImages function error:', e.message);
|
|
}
|
|
}
|
|
|
|
// Try accessing the image discovery result directly from game properties
|
|
if (taskImages.length === 0) {
|
|
console.log(' Checking for image discovery results in game properties...');
|
|
// Look for properties that might contain the discovered images
|
|
const gameProps = Object.keys(window.game);
|
|
const imageProps = gameProps.filter(prop => prop.toLowerCase().includes('image') || prop.toLowerCase().includes('task') || prop.toLowerCase().includes('consequence'));
|
|
console.log(' Image-related properties:', imageProps);
|
|
|
|
// Try common property names where images might be stored
|
|
const possibleTaskProps = ['taskImageList', 'allTaskImages', 'loadedTaskImages', 'imageList', 'taskImageCache'];
|
|
const possibleConsProps = ['consequenceImageList', 'allConsequenceImages', 'loadedConsequenceImages', 'consequenceImageCache'];
|
|
|
|
for (const prop of possibleTaskProps) {
|
|
if (window.game[prop] && Array.isArray(window.game[prop])) {
|
|
console.log(` Found task images in ${prop}:`, window.game[prop].length);
|
|
taskImages = window.game[prop];
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const prop of possibleConsProps) {
|
|
if (window.game[prop] && Array.isArray(window.game[prop])) {
|
|
console.log(` Found consequence images in ${prop}:`, window.game[prop].length);
|
|
consequenceImages = window.game[prop];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (consequenceImages.length === 0) {
|
|
consequenceImages = window.game.discoveredConsequenceImages || window.game.consequenceImages || [];
|
|
}
|
|
|
|
console.log(` Found ${taskImages.length} task images, ${consequenceImages.length} consequence images`);
|
|
console.log(' Game data structure:', Object.keys(window.game));
|
|
console.log(' DataManager structure:', window.game.dataManager ? Object.keys(window.game.dataManager) : 'not available');
|
|
console.log(' FileManager structure:', window.game.fileManager ? Object.keys(window.game.fileManager) : 'not available');
|
|
console.log(' Checking specific image properties:');
|
|
console.log(' - gameData:', !!window.game.gameData);
|
|
console.log(' - dataManager.gameData:', !!(window.game.dataManager && window.game.dataManager.gameData));
|
|
console.log(' - fileManager:', !!window.game.fileManager);
|
|
console.log(' - taskImagePaths:', !!window.game.taskImagePaths);
|
|
console.log(' - consequenceImagePaths:', !!window.game.consequenceImagePaths);
|
|
console.log(' - discoveredTaskImages:', !!window.game.discoveredTaskImages);
|
|
console.log(' - discoveredConsequenceImages:', !!window.game.discoveredConsequenceImages);
|
|
|
|
// Detailed debugging of available image data
|
|
if (window.game.dataManager && window.game.dataManager.gameData) {
|
|
console.log(' DataManager gameData keys:', Object.keys(window.game.dataManager.gameData));
|
|
console.log(' DataManager task images:', window.game.dataManager.gameData.taskImages?.length || 0);
|
|
console.log(' DataManager consequence images:', window.game.dataManager.gameData.consequenceImages?.length || 0);
|
|
}
|
|
|
|
if (window.game.fileManager) {
|
|
console.log(' FileManager properties:', Object.keys(window.game.fileManager));
|
|
if (window.game.fileManager.imageDirectories) {
|
|
console.log(' FileManager image directories:', window.game.fileManager.imageDirectories);
|
|
|
|
// Debug available methods
|
|
console.log(' Available electronAPI methods:', window.electronAPI ? Object.keys(window.electronAPI) : 'electronAPI not available');
|
|
console.log(' Available desktopFileManager methods:', window.desktopFileManager ? Object.keys(window.desktopFileManager) : 'desktopFileManager not available');
|
|
console.log(' Available fileManager methods:', Object.keys(window.game.fileManager));
|
|
|
|
// Try to directly scan the directories if possible
|
|
if (taskImages.length === 0) {
|
|
console.log(' Attempting direct directory scan...');
|
|
try {
|
|
const taskDir = window.game.fileManager.imageDirectories.tasks;
|
|
const consDir = window.game.fileManager.imageDirectories.consequences;
|
|
|
|
console.log(' Task directory:', taskDir);
|
|
console.log(' Consequence directory:', consDir);
|
|
|
|
// Try different methods to scan directories
|
|
if (taskDir) {
|
|
// Method 1: electronAPI.readDirectory (correct method name)
|
|
if (window.electronAPI && window.electronAPI.readDirectory) {
|
|
try {
|
|
const taskFilesPromise = window.electronAPI.readDirectory(taskDir);
|
|
console.log(' Raw task files from readDirectory:', taskFilesPromise);
|
|
|
|
// Handle async result
|
|
if (taskFilesPromise && typeof taskFilesPromise.then === 'function') {
|
|
const taskFiles = await taskFilesPromise;
|
|
console.log(' Resolved task files:', taskFiles);
|
|
if (taskFiles && taskFiles.length > 0) {
|
|
// Filter for image files - check if it's an object with name property or just a string
|
|
const imageFiles = taskFiles.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(fileName);
|
|
});
|
|
console.log(' Filtered task image files:', imageFiles);
|
|
if (imageFiles.length > 0) {
|
|
taskImages = imageFiles.map(file => {
|
|
if (typeof file === 'object' && file.name && file.path) {
|
|
// Already has name and path
|
|
return { path: file.path, name: file.name };
|
|
} else {
|
|
// Just a filename string
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return {
|
|
path: window.electronAPI.pathJoin ? window.electronAPI.pathJoin(taskDir, fileName) : `${taskDir}\\${fileName}`,
|
|
name: fileName
|
|
};
|
|
}
|
|
});
|
|
console.log(` ReadDirectory found ${taskImages.length} task images`);
|
|
}
|
|
}
|
|
} else if (taskFilesPromise && Array.isArray(taskFilesPromise)) {
|
|
// Handle sync result
|
|
const imageFiles = taskFilesPromise.filter(file =>
|
|
/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file)
|
|
);
|
|
if (imageFiles.length > 0) {
|
|
taskImages = imageFiles.map(file => ({
|
|
path: window.electronAPI.pathJoin ? window.electronAPI.pathJoin(taskDir, file) : `${taskDir}\\${file}`,
|
|
name: file
|
|
}));
|
|
console.log(` ReadDirectory found ${taskImages.length} task images (sync)`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(' readDirectory error for tasks:', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (consDir) {
|
|
// Method 1: electronAPI.readDirectory (correct method name)
|
|
if (window.electronAPI && window.electronAPI.readDirectory) {
|
|
try {
|
|
const consFilesPromise = window.electronAPI.readDirectory(consDir);
|
|
console.log(' Raw consequence files from readDirectory:', consFilesPromise);
|
|
|
|
// Handle async result
|
|
if (consFilesPromise && typeof consFilesPromise.then === 'function') {
|
|
const consFiles = await consFilesPromise;
|
|
console.log(' Resolved consequence files:', consFiles);
|
|
if (consFiles && consFiles.length > 0) {
|
|
// Filter for image files - check if it's an object with name property or just a string
|
|
const imageFiles = consFiles.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(fileName);
|
|
});
|
|
console.log(' Filtered consequence image files:', imageFiles);
|
|
if (imageFiles.length > 0) {
|
|
consequenceImages = imageFiles.map(file => {
|
|
if (typeof file === 'object' && file.name && file.path) {
|
|
// Already has name and path
|
|
return { path: file.path, name: file.name };
|
|
} else {
|
|
// Just a filename string
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return {
|
|
path: window.electronAPI.pathJoin ? window.electronAPI.pathJoin(consDir, fileName) : `${consDir}\\${fileName}`,
|
|
name: fileName
|
|
};
|
|
}
|
|
});
|
|
console.log(` ReadDirectory found ${consequenceImages.length} consequence images`);
|
|
}
|
|
}
|
|
} else if (consFilesPromise && Array.isArray(consFilesPromise)) {
|
|
// Handle sync result
|
|
const imageFiles = consFilesPromise.filter(file =>
|
|
/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file)
|
|
);
|
|
if (imageFiles.length > 0) {
|
|
consequenceImages = imageFiles.map(file => ({
|
|
path: window.electronAPI.pathJoin ? window.electronAPI.pathJoin(consDir, file) : `${consDir}\\${file}`,
|
|
name: file
|
|
}));
|
|
console.log(` ReadDirectory found ${consequenceImages.length} consequence images (sync)`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(' readDirectory error for consequences:', e.message);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(' Direct directory scan error:', e.message);
|
|
}
|
|
}
|
|
}
|
|
if (window.game.fileManager.getTaskImages) {
|
|
console.log(' FileManager getTaskImages available');
|
|
try {
|
|
const taskImgs = window.game.fileManager.getTaskImages();
|
|
console.log(' FileManager task images:', taskImgs?.length || 0);
|
|
if (taskImgs && taskImgs.length > 0) {
|
|
taskImages = taskImgs;
|
|
}
|
|
} catch (e) {
|
|
console.log(' FileManager getTaskImages error:', e.message);
|
|
}
|
|
}
|
|
if (window.game.fileManager.getConsequenceImages) {
|
|
console.log(' FileManager getConsequenceImages available');
|
|
try {
|
|
const consImgs = window.game.fileManager.getConsequenceImages();
|
|
console.log(' FileManager consequence images:', consImgs?.length || 0);
|
|
if (consImgs && consImgs.length > 0) {
|
|
consequenceImages = consImgs;
|
|
}
|
|
} catch (e) {
|
|
console.log(' FileManager getConsequenceImages error:', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` FINAL RESULT: ${taskImages.length} task images, ${consequenceImages.length} consequence images`);
|
|
|
|
// Update image count display
|
|
const imageCountElement = document.getElementById('lib-image-count');
|
|
if (imageCountElement) {
|
|
imageCountElement.textContent = `${taskImages.length + consequenceImages.length} images`;
|
|
}
|
|
|
|
// Populate image gallery
|
|
const imageGallery = document.getElementById('lib-image-gallery');
|
|
if (imageGallery && (taskImages.length > 0 || consequenceImages.length > 0)) {
|
|
imageGallery.innerHTML = '';
|
|
|
|
// Add task images
|
|
taskImages.forEach((image, index) => {
|
|
const imgElement = document.createElement('div');
|
|
imgElement.className = 'gallery-item';
|
|
imgElement.innerHTML = `
|
|
<img src="${image.path}" alt="Task Image ${index + 1}" style="width: 100%; height: 150px; object-fit: cover; border-radius: 8px;">
|
|
<div class="gallery-item-info">
|
|
<span class="gallery-item-type">Task</span>
|
|
<span class="gallery-item-name">${image.name}</span>
|
|
</div>
|
|
`;
|
|
imageGallery.appendChild(imgElement);
|
|
});
|
|
|
|
// Add consequence images
|
|
consequenceImages.forEach((image, index) => {
|
|
const imgElement = document.createElement('div');
|
|
imgElement.className = 'gallery-item';
|
|
imgElement.innerHTML = `
|
|
<img src="${image.path}" alt="Consequence Image ${index + 1}" style="width: 100%; height: 150px; object-fit: cover; border-radius: 8px;">
|
|
<div class="gallery-item-info">
|
|
<span class="gallery-item-type">Consequence</span>
|
|
<span class="gallery-item-name">${image.name}</span>
|
|
</div>
|
|
`;
|
|
imageGallery.appendChild(imgElement);
|
|
});
|
|
|
|
console.log(`? Created ${taskImages.length + consequenceImages.length} image gallery items`);
|
|
} else if (imageGallery) {
|
|
imageGallery.innerHTML = `
|
|
<div class="no-images-message">
|
|
<p>? No images found</p>
|
|
<p>Import images to get started</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Set up image directory management buttons
|
|
const addImageDirBtn = document.getElementById('lib-add-image-directory-btn');
|
|
const addIndividualImagesBtn = document.getElementById('lib-add-individual-images-btn');
|
|
const refreshImageDirBtn = document.getElementById('lib-refresh-image-directories-btn');
|
|
const clearImageDirBtn = document.getElementById('lib-clear-image-directories-btn');
|
|
|
|
if (addImageDirBtn) {
|
|
addImageDirBtn.onclick = () => {
|
|
console.log('Adding image directory...');
|
|
handleAddImageDirectory();
|
|
};
|
|
}
|
|
|
|
if (addIndividualImagesBtn) {
|
|
addIndividualImagesBtn.onclick = () => {
|
|
console.log('Adding individual images...');
|
|
handleAddIndividualImages();
|
|
};
|
|
}
|
|
|
|
if (refreshImageDirBtn) {
|
|
refreshImageDirBtn.onclick = () => {
|
|
console.log('Refreshing image directories...');
|
|
handleRefreshImageDirectories();
|
|
};
|
|
}
|
|
|
|
if (clearImageDirBtn) {
|
|
clearImageDirBtn.onclick = () => {
|
|
console.log('Clearing image directories...');
|
|
handleClearImageDirectories();
|
|
};
|
|
}
|
|
|
|
// Set up category filter dropdown
|
|
const categoryFilter = document.getElementById('lib-image-category-filter');
|
|
if (categoryFilter) {
|
|
categoryFilter.onchange = () => {
|
|
console.log('Filtering images by category:', categoryFilter.value);
|
|
// Use stored images and apply filter
|
|
if (window.allLinkedImages) {
|
|
const filteredImages = filterImagesByCategory(window.allLinkedImages, categoryFilter.value);
|
|
populateImageGallery(filteredImages);
|
|
|
|
// Update count display
|
|
const countElement = document.getElementById('lib-image-count');
|
|
if (countElement) {
|
|
countElement.textContent = `${filteredImages.length} images`;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Initialize linked image directories display
|
|
updateImageDirectoriesList();
|
|
|
|
// Check if we have linked directories first
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
|
|
if (linkedDirs.length > 0) {
|
|
console.log(' Using linked directories instead of built-in directories');
|
|
await loadLinkedImages();
|
|
return; // Skip built-in directory scanning
|
|
} else {
|
|
console.log(' No linked directories found, using built-in directories');
|
|
}
|
|
}
|
|
|
|
function setupLibraryAudioTab(retryCount = 0) {
|
|
console.log('Setting up audio tab functionality...');
|
|
|
|
// Wait for game to be available (max 10 retries)
|
|
if (!window.game && retryCount < 10) {
|
|
console.log(`? Game not ready yet, retrying in 500ms... (attempt ${retryCount + 1}/10)`);
|
|
setTimeout(() => setupLibraryAudioTab(retryCount + 1), 500);
|
|
return;
|
|
}
|
|
|
|
if (!window.game) {
|
|
console.error('? Game not available after 10 retries, aborting audio tab setup');
|
|
return;
|
|
}
|
|
|
|
// Get current audio stats from multiple possible locations
|
|
let audioLibrary = {};
|
|
let backgroundTracks = [];
|
|
let ambientTracks = [];
|
|
|
|
// Try audioManager first
|
|
if (window.game.audioManager && window.game.audioManager.audioLibrary) {
|
|
audioLibrary = window.game.audioManager.audioLibrary;
|
|
backgroundTracks = audioLibrary.background || [];
|
|
ambientTracks = audioLibrary.ambient || [];
|
|
}
|
|
|
|
// If audioLibrary is empty, try checking if tracks are stored differently
|
|
if (backgroundTracks.length === 0 && window.game.audioManager) {
|
|
// Check if background tracks are stored in a different property
|
|
backgroundTracks = window.game.audioManager.backgroundTracks ||
|
|
window.game.audioManager.background || [];
|
|
}
|
|
|
|
if (ambientTracks.length === 0 && window.game.audioManager) {
|
|
// Check if ambient tracks are stored in a different property
|
|
ambientTracks = window.game.audioManager.ambientTracks ||
|
|
window.game.audioManager.ambient || [];
|
|
}
|
|
|
|
console.log(` Found ${backgroundTracks.length} background tracks, ${ambientTracks.length} ambient tracks`);
|
|
console.log(' Audio manager available:', !!window.game.audioManager);
|
|
|
|
// Store audio tracks globally for filtering
|
|
window.allAudioTracks = { background: backgroundTracks, ambient: ambientTracks };
|
|
|
|
// Apply current filter and populate the gallery
|
|
const categoryFilter = document.getElementById('lib-audio-category-filter');
|
|
const selectedCategory = categoryFilter ? categoryFilter.value : 'all';
|
|
const filteredTracks = filterAudioByCategory(backgroundTracks, ambientTracks, selectedCategory);
|
|
populateAudioGallery(filteredTracks.background, filteredTracks.ambient);
|
|
|
|
// Update count display
|
|
const audioCountElement = document.getElementById('lib-audio-count');
|
|
if (audioCountElement) {
|
|
const totalCount = filteredTracks.background.length + filteredTracks.ambient.length;
|
|
audioCountElement.textContent = `${totalCount} files`;
|
|
}
|
|
|
|
// Set up category filter dropdown
|
|
if (categoryFilter) {
|
|
categoryFilter.onchange = () => {
|
|
console.log('Filtering audio by category:', categoryFilter.value);
|
|
// Use stored audio tracks and apply filter
|
|
if (window.allAudioTracks) {
|
|
const filtered = filterAudioByCategory(
|
|
window.allAudioTracks.background,
|
|
window.allAudioTracks.ambient,
|
|
categoryFilter.value
|
|
);
|
|
populateAudioGallery(filtered.background, filtered.ambient);
|
|
|
|
// Update count display
|
|
const totalCount = filtered.background.length + filtered.ambient.length;
|
|
audioCountElement.textContent = `${totalCount} files`;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function filterAudioByCategory(backgroundTracks, ambientTracks, category) {
|
|
switch (category) {
|
|
case 'background':
|
|
return { background: backgroundTracks, ambient: [] };
|
|
case 'ambient':
|
|
return { background: [], ambient: ambientTracks };
|
|
case 'all':
|
|
default:
|
|
return { background: backgroundTracks, ambient: ambientTracks };
|
|
}
|
|
}
|
|
|
|
function populateAudioGallery(backgroundTracks, ambientTracks) {
|
|
const audioGallery = document.getElementById('lib-audio-gallery');
|
|
if (!audioGallery) return;
|
|
|
|
if (backgroundTracks.length === 0 && ambientTracks.length === 0) {
|
|
audioGallery.innerHTML = `
|
|
<div class="no-audio-message">
|
|
<p> No audio files found</p>
|
|
<p>Import audio to get started</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
audioGallery.innerHTML = '';
|
|
|
|
// Add background tracks
|
|
backgroundTracks.forEach((track, index) => {
|
|
const audioElement = document.createElement('div');
|
|
audioElement.className = 'gallery-item audio-item';
|
|
audioElement.innerHTML = `
|
|
<div class="audio-info">
|
|
<div class="audio-icon"></div>
|
|
<div class="audio-name">${track.name || `Track ${index + 1}`}</div>
|
|
<div class="audio-type">Background</div>
|
|
</div>
|
|
`;
|
|
audioGallery.appendChild(audioElement);
|
|
});
|
|
|
|
// Add ambient tracks
|
|
ambientTracks.forEach((track, index) => {
|
|
const audioElement = document.createElement('div');
|
|
audioElement.className = 'gallery-item audio-item';
|
|
audioElement.innerHTML = `
|
|
<div class="audio-info">
|
|
<div class="audio-icon"></div>
|
|
<div class="audio-name">${track.name || `Ambient ${index + 1}`}</div>
|
|
<div class="audio-type">Ambient</div>
|
|
</div>
|
|
`;
|
|
audioGallery.appendChild(audioElement);
|
|
});
|
|
|
|
console.log(`? Created ${backgroundTracks.length + ambientTracks.length} audio gallery items`);
|
|
}
|
|
|
|
async function setupLibraryVideoTab(retryCount = 0) {
|
|
console.log('Setting up video tab functionality...');
|
|
|
|
// Wait for game to be available (max 10 retries)
|
|
if (!window.game && retryCount < 10) {
|
|
console.log(`? Game not ready yet, retrying in 500ms... (attempt ${retryCount + 1}/10)`);
|
|
setTimeout(() => setupLibraryVideoTab(retryCount + 1), 500);
|
|
return;
|
|
}
|
|
|
|
if (!window.game) {
|
|
console.error('? Game not available after 10 retries, aborting video tab setup');
|
|
return;
|
|
}
|
|
|
|
// Get current video stats from multiple possible locations
|
|
let videoLibrary = {};
|
|
let videoManager = null;
|
|
|
|
// Try different video manager locations
|
|
if (window.game.videoPlayerManager) {
|
|
videoManager = window.game.videoPlayerManager;
|
|
videoLibrary = videoManager.videoLibrary || {};
|
|
} else if (window.game.videoManager) {
|
|
videoManager = window.game.videoManager;
|
|
videoLibrary = videoManager.videoLibrary || {};
|
|
} else if (window.videoPlayerManager) {
|
|
// Check global videoPlayerManager
|
|
videoManager = window.videoPlayerManager;
|
|
videoLibrary = videoManager.videoLibrary || {};
|
|
}
|
|
|
|
// If still no video library found, try checking desktop file manager
|
|
if (Object.keys(videoLibrary).length === 0 && window.game.fileManager) {
|
|
// Desktop file manager might have video data
|
|
const fileManager = window.game.fileManager;
|
|
if (fileManager.getAllVideos) {
|
|
const allVideos = fileManager.getAllVideos();
|
|
// Convert to expected format
|
|
videoLibrary = {
|
|
background: allVideos.filter(v => v.category === 'background') || [],
|
|
task: allVideos.filter(v => v.category === 'task') || [],
|
|
reward: allVideos.filter(v => v.category === 'reward') || [],
|
|
punishment: allVideos.filter(v => v.category === 'punishment') || []
|
|
};
|
|
}
|
|
}
|
|
|
|
// Try using the desktop file manager video system directly
|
|
if (Object.keys(videoLibrary).length === 0 && window.desktopFileManager) {
|
|
console.log(' Trying desktop file manager for videos...');
|
|
try {
|
|
const allVideos = window.desktopFileManager.getAllVideos();
|
|
console.log(` Desktop file manager found ${allVideos.length} videos`);
|
|
|
|
if (allVideos.length > 0) {
|
|
// Group videos by type/category if available
|
|
videoLibrary = {
|
|
background: allVideos.filter(v => v.type === 'background' || v.category === 'background') || [],
|
|
task: allVideos.filter(v => v.type === 'task' || v.category === 'task') || [],
|
|
reward: allVideos.filter(v => v.type === 'reward' || v.category === 'reward') || [],
|
|
punishment: allVideos.filter(v => v.type === 'punishment' || v.category === 'punishment') || [],
|
|
all: allVideos // Keep all videos as backup
|
|
};
|
|
console.log(` Grouped videos: ${videoLibrary.background.length} bg, ${videoLibrary.task.length} task, ${videoLibrary.reward.length} reward, ${videoLibrary.punishment.length} punishment`);
|
|
}
|
|
} catch (e) {
|
|
console.log(' Desktop file manager video error:', e.message);
|
|
}
|
|
}
|
|
|
|
// Try accessing allLinkedVideos directly if no videos found yet
|
|
if (Object.keys(videoLibrary).length === 0 && window.game.fileManager.allLinkedVideos) {
|
|
console.log(' Trying allLinkedVideos from fileManager...');
|
|
const linkedVideos = window.game.fileManager.allLinkedVideos;
|
|
console.log(` AllLinkedVideos found ${linkedVideos.length} videos`);
|
|
|
|
if (linkedVideos.length > 0) {
|
|
videoLibrary = {
|
|
background: linkedVideos.filter(v => v.type === 'background' || v.category === 'background') || [],
|
|
task: linkedVideos.filter(v => v.type === 'task' || v.category === 'task') || [],
|
|
reward: linkedVideos.filter(v => v.type === 'reward' || v.category === 'reward') || [],
|
|
punishment: linkedVideos.filter(v => v.type === 'punishment' || v.category === 'punishment') || [],
|
|
all: linkedVideos // Keep all videos as backup
|
|
};
|
|
console.log(` AllLinkedVideos grouped: ${videoLibrary.background.length} bg, ${videoLibrary.task.length} task, ${videoLibrary.reward.length} reward, ${videoLibrary.punishment.length} punishment`);
|
|
}
|
|
}
|
|
|
|
const backgroundVideos = videoLibrary.background || [];
|
|
const taskVideos = videoLibrary.task || [];
|
|
const rewardVideos = videoLibrary.reward || [];
|
|
const punishmentVideos = videoLibrary.punishment || [];
|
|
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
|
|
|
let totalVideos = backgroundVideos.length + taskVideos.length + rewardVideos.length + punishmentVideos.length + capturedVideos.length;
|
|
|
|
// If no categorized videos found but we have "all" videos, use those
|
|
let allVideos = [];
|
|
if (totalVideos === 0 && videoLibrary.all && Array.isArray(videoLibrary.all)) {
|
|
allVideos = videoLibrary.all;
|
|
totalVideos = allVideos.length;
|
|
console.log(` Using ${totalVideos} uncategorized videos from 'all' array`);
|
|
}
|
|
|
|
console.log(` Found ${backgroundVideos.length} background, ${taskVideos.length} task, ${rewardVideos.length} reward, ${punishmentVideos.length} punishment videos`);
|
|
console.log(' Video manager available:', !!videoManager);
|
|
console.log(' Video manager types:', Object.keys(window.game).filter(key => key.toLowerCase().includes('video')));
|
|
|
|
// Update video count display
|
|
const videoCountElement = document.getElementById('lib-video-count');
|
|
if (videoCountElement) {
|
|
videoCountElement.textContent = `${totalVideos} files`;
|
|
}
|
|
|
|
// Populate video gallery
|
|
const videoGallery = document.getElementById('lib-video-gallery');
|
|
if (videoGallery && totalVideos > 0) {
|
|
videoGallery.innerHTML = '';
|
|
|
|
// Add background videos
|
|
backgroundVideos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-info">
|
|
<div class="video-icon"></div>
|
|
<div class="video-name">${video.name || `Background ${index + 1}`}</div>
|
|
<div class="video-type">Background</div>
|
|
</div>
|
|
`;
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
|
|
// Add task videos
|
|
taskVideos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-info">
|
|
<div class="video-icon"></div>
|
|
<div class="video-name">${video.name || `Task ${index + 1}`}</div>
|
|
<div class="video-type">Task</div>
|
|
</div>
|
|
`;
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
|
|
// Add reward videos
|
|
rewardVideos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-info">
|
|
<div class="video-icon">?</div>
|
|
<div class="video-name">${video.name || `Reward ${index + 1}`}</div>
|
|
<div class="video-type">Reward</div>
|
|
</div>
|
|
`;
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
|
|
// Add punishment videos
|
|
punishmentVideos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-info">
|
|
<div class="video-icon">?</div>
|
|
<div class="video-name">${video.name || `Punishment ${index + 1}`}</div>
|
|
<div class="video-type">Punishment</div>
|
|
</div>
|
|
`;
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
|
|
// If no categorized videos were added but we have uncategorized videos, add those
|
|
if (videoGallery.children.length === 0 && allVideos.length > 0) {
|
|
allVideos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-info">
|
|
<div class="video-icon"></div>
|
|
<div class="video-name">${video.name || video.fileName || `Video ${index + 1}`}</div>
|
|
<div class="video-type">${video.type || video.category || 'Video'}</div>
|
|
</div>
|
|
`;
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
}
|
|
|
|
console.log(`? Created ${totalVideos} video gallery items`);
|
|
} else if (videoGallery) {
|
|
videoGallery.innerHTML = `
|
|
<div class="no-video-message">
|
|
<p> No video files found</p>
|
|
<p>Import videos to get started</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Set up video directory management buttons
|
|
const addVideoDirBtn = document.getElementById('lib-add-video-directory-btn');
|
|
const addIndividualVideosBtn = document.getElementById('lib-add-individual-videos-btn');
|
|
const refreshVideoDirBtn = document.getElementById('lib-refresh-video-directories-btn');
|
|
const clearVideoDirBtn = document.getElementById('lib-clear-video-directories-btn');
|
|
|
|
if (addVideoDirBtn) {
|
|
addVideoDirBtn.onclick = () => {
|
|
console.log('Adding video directory...');
|
|
handleAddVideoDirectory();
|
|
};
|
|
}
|
|
|
|
if (addIndividualVideosBtn) {
|
|
addIndividualVideosBtn.onclick = () => {
|
|
console.log('Adding individual videos...');
|
|
handleAddIndividualVideos();
|
|
};
|
|
}
|
|
|
|
if (refreshVideoDirBtn) {
|
|
refreshVideoDirBtn.onclick = () => {
|
|
console.log('Refreshing video directories...');
|
|
handleRefreshVideoDirectories();
|
|
};
|
|
}
|
|
|
|
if (clearVideoDirBtn) {
|
|
clearVideoDirBtn.onclick = () => {
|
|
console.log('Clearing video directories...');
|
|
handleClearVideoDirectories();
|
|
};
|
|
}
|
|
|
|
// Initialize linked video directories display
|
|
updateVideoDirectoriesList();
|
|
|
|
// Load linked videos if any exist
|
|
let linkedVideoDirs;
|
|
let individualVideos;
|
|
try {
|
|
linkedVideoDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedVideoDirs)) {
|
|
linkedVideoDirs = [];
|
|
}
|
|
individualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]');
|
|
if (!Array.isArray(individualVideos)) {
|
|
individualVideos = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing video directories, resetting to empty arrays:', e);
|
|
linkedVideoDirs = [];
|
|
individualVideos = [];
|
|
}
|
|
|
|
if (linkedVideoDirs.length > 0 || individualVideos.length > 0) {
|
|
console.log(' Loading linked video directories...');
|
|
loadLinkedVideos();
|
|
}
|
|
}
|
|
|
|
function setupLibraryGalleryTab() {
|
|
console.log('Setting up gallery tab functionality...');
|
|
|
|
// Load captured photos from localStorage
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
console.log(` Found ${capturedPhotos.length} captured photos`);
|
|
|
|
// Load verification photos from localStorage
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
console.log(` Found ${verificationPhotos.length} verification photos`);
|
|
|
|
const allPhotosGrid = document.getElementById('lib-all-photos-grid');
|
|
const allPhotosCount = document.getElementById('lib-all-photos-count');
|
|
|
|
if (allPhotosGrid) {
|
|
if (capturedPhotos.length === 0 && verificationPhotos.length === 0) {
|
|
allPhotosGrid.innerHTML = `
|
|
<div class="no-photos-message">
|
|
<p> No photos found</p>
|
|
<p>Take some photos during gameplay to see them here</p>
|
|
</div>
|
|
`;
|
|
if (allPhotosCount) allPhotosCount.textContent = '0 photos';
|
|
} else {
|
|
// Create photo gallery grid - start with captured photos
|
|
let photosHtml = '';
|
|
capturedPhotos.forEach((photo, index) => {
|
|
const timestamp = new Date(photo.timestamp || Date.now()).toLocaleDateString();
|
|
const imageData = photo.imageData || photo.dataURL; // Support both formats
|
|
|
|
if (imageData) {
|
|
photosHtml += `
|
|
<div class="photo-item" data-index="${index}" data-type="captured">
|
|
<div class="photo-container">
|
|
<div class="photo-checkbox">
|
|
<input type="checkbox" id="photo-${index}" class="photo-select" data-index="${index}" onchange="updateSelectionCount()">
|
|
<label for="photo-${index}" class="checkbox-label"></label>
|
|
</div>
|
|
<img src="${imageData}" alt="Captured Photo ${index + 1}"
|
|
onclick="showPhotoPreview('${imageData}', 'Photo ${index + 1}')">
|
|
<div class="photo-actions">
|
|
<button class="photo-download-btn" onclick="downloadSinglePhoto(${index})" title="Download Photo">
|
|
|
|
</button>
|
|
<button class="photo-delete-btn" onclick="deletePhoto(${index})" title="Delete Photo">
|
|
?
|
|
</button>
|
|
</div>
|
|
<div class="photo-info">
|
|
<span class="photo-date">${timestamp}</span>
|
|
<span class="photo-type">${photo.sessionType || 'Training'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Add verification photos to the gallery
|
|
verificationPhotos.forEach((photo, index) => {
|
|
const timestamp = new Date(photo.timestamp || Date.now()).toLocaleDateString();
|
|
const photoType = photo.phase === 'start' ? ' START Position' : ' END Position';
|
|
const degradingMessage = photo.message || 'Position verification photo';
|
|
const verificationIndex = capturedPhotos.length + index; // Offset by captured photos length
|
|
const imageData = photo.data || photo.dataUrl; // Support both formats
|
|
|
|
if (imageData) {
|
|
photosHtml += `
|
|
<div class="photo-item verification-photo-item" data-index="${verificationIndex}" data-type="verification">
|
|
<div class="photo-container verification-photo-container">
|
|
<div class="photo-checkbox">
|
|
<input type="checkbox" id="verification-photo-${index}" class="photo-select verification-photo-select" data-index="${verificationIndex}" data-verification-index="${index}" onchange="updateSelectionCount()">
|
|
<label for="verification-photo-${index}" class="checkbox-label"></label>
|
|
</div>
|
|
<img src="${imageData}" alt="Verification Photo ${index + 1}"
|
|
onclick="showVerificationPhotoPreview('${imageData}', 'Verification Photo ${index + 1}', '${degradingMessage}', ${photo.phase === 'start'}, '${photo.timestamp}')">
|
|
<div class="photo-actions">
|
|
<button class="photo-download-btn" onclick="downloadVerificationPhoto(${index})" title="Download Verification Photo">
|
|
|
|
</button>
|
|
<button class="photo-delete-btn" onclick="deleteVerificationPhoto(${index})" title="Delete Verification Photo">
|
|
?
|
|
</button>
|
|
</div>
|
|
<div class="photo-info verification-photo-info">
|
|
<span class="photo-date">${timestamp}</span>
|
|
<span class="photo-type">${photoType}</span>
|
|
<span class="verification-message">"${degradingMessage}"</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Add captured videos to the gallery
|
|
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
|
console.log(` Found ${capturedVideos.length} captured videos`);
|
|
|
|
capturedVideos.forEach((video, index) => {
|
|
const timestamp = new Date(video.timestamp || Date.now()).toLocaleDateString();
|
|
const duration = formatVideoDuration(video.duration);
|
|
|
|
photosHtml += `
|
|
<div class="photo-item video-item" data-video-id="${video.id}">
|
|
<div class="photo-container">
|
|
<div class="photo-checkbox">
|
|
<input type="checkbox" id="video-${index}" class="video-select" data-video-id="${video.id}" onchange="updateSelectionCount()">
|
|
<label for="video-${index}" class="checkbox-label"></label>
|
|
</div>
|
|
<div class="video-thumbnail-wrapper" onclick="playCapturedVideo('${video.id}')"
|
|
style="position: relative; cursor: pointer;"
|
|
onmouseover="this.querySelector('.video-play-overlay').style.opacity='1'"
|
|
onmouseout="this.querySelector('.video-play-overlay').style.opacity='0'">
|
|
${video.thumbnail ?
|
|
`<img src="${video.thumbnail}" alt="Video Thumbnail" style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px;">` :
|
|
`<div style="width: 100%; height: 120px; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; border-radius: 8px; color: #fff;"></div>`
|
|
}
|
|
<div class="video-play-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.3s; border-radius: 8px;">
|
|
<div class="play-icon-small" style="color: white; font-size: 24px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="photo-actions">
|
|
<button class="photo-download-btn" onclick="downloadCapturedVideo('${video.id}')" title="Download Video">
|
|
|
|
</button>
|
|
<button class="photo-delete-btn" onclick="deleteCapturedVideoFromGallery('${video.id}')" title="Delete Video">
|
|
?
|
|
</button>
|
|
</div>
|
|
<div class="photo-info">
|
|
<span class="photo-date">${timestamp}</span>
|
|
<span class="photo-type">Session Video � ${duration}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
allPhotosGrid.innerHTML = photosHtml;
|
|
const totalPhotos = capturedPhotos.length + verificationPhotos.length;
|
|
const totalItems = totalPhotos + capturedVideos.length;
|
|
if (allPhotosCount) allPhotosCount.textContent = `${totalPhotos} photos, ${capturedVideos.length} videos`;
|
|
}
|
|
}
|
|
|
|
// Also populate dress-up photos if they exist
|
|
const dressUpPhotos = capturedPhotos.filter(photo =>
|
|
photo.sessionType && photo.sessionType.includes('dress-up'));
|
|
|
|
const dressUpGrid = document.getElementById('lib-dress-up-photos-grid');
|
|
const dressUpCount = document.getElementById('lib-dress-up-photos-count');
|
|
|
|
if (dressUpGrid && dressUpPhotos.length > 0) {
|
|
let dressUpHtml = '';
|
|
dressUpPhotos.forEach((photo, index) => {
|
|
const timestamp = new Date(photo.timestamp || Date.now()).toLocaleDateString();
|
|
const imageData = photo.imageData || photo.dataURL; // Support both formats
|
|
|
|
if (imageData) {
|
|
// Find the original index in the full capturedPhotos array
|
|
const originalIndex = capturedPhotos.findIndex(p => p.imageData === imageData || p.dataURL === imageData);
|
|
dressUpHtml += `
|
|
<div class="photo-item" data-index="${originalIndex}">
|
|
<div class="photo-container">
|
|
<div class="photo-checkbox">
|
|
<input type="checkbox" id="photo-${originalIndex}" class="photo-select" data-index="${originalIndex}" onchange="updateSelectionCount()">
|
|
<label for="photo-${originalIndex}" class="checkbox-label"></label>
|
|
</div>
|
|
<img src="${imageData}" alt="Dress Up Photo ${index + 1}"
|
|
onclick="showPhotoPreview('${imageData}', 'Dress Up Photo ${index + 1}')">
|
|
<div class="photo-actions">
|
|
<button class="photo-download-btn" onclick="downloadSinglePhoto(${originalIndex})" title="Download Photo">
|
|
|
|
</button>
|
|
<button class="photo-delete-btn" onclick="deletePhoto(${originalIndex})" title="Delete Photo">
|
|
?
|
|
</button>
|
|
</div>
|
|
<div class="photo-info">
|
|
<span class="photo-date">${timestamp}</span>
|
|
<span class="photo-type">${photo.sessionType}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
dressUpGrid.innerHTML = dressUpHtml;
|
|
if (dressUpCount) dressUpCount.textContent = `${dressUpPhotos.length} photos`;
|
|
}
|
|
|
|
// Initialize bulk action event listeners
|
|
setTimeout(initializeBulkActions, 100);
|
|
}
|
|
|
|
// Delete a photo from the gallery
|
|
function deletePhoto(index) {
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
|
|
if (index < 0 || index >= capturedPhotos.length) {
|
|
console.error('Invalid photo index:', index);
|
|
return;
|
|
}
|
|
|
|
const photo = capturedPhotos[index];
|
|
const photoType = photo.sessionType || 'Training';
|
|
const photoDate = new Date(photo.timestamp || Date.now()).toLocaleDateString();
|
|
|
|
// Show confirmation dialog
|
|
const confirmed = confirm(`Are you sure you want to delete this photo?\n\nType: ${photoType}\nDate: ${photoDate}\n\nThis action cannot be undone.`);
|
|
|
|
if (confirmed) {
|
|
// Remove photo from array
|
|
capturedPhotos.splice(index, 1);
|
|
|
|
// Update localStorage
|
|
localStorage.setItem('capturedPhotos', JSON.stringify(capturedPhotos));
|
|
|
|
// Show success message
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Photo deleted successfully!`, 'info');
|
|
}
|
|
|
|
// Refresh the photo galleries
|
|
setupLibraryGalleryTab();
|
|
|
|
console.log(`? Deleted photo ${index + 1} (${photoType})`);
|
|
}
|
|
}
|
|
|
|
// Update selection count and enable/disable bulk action buttons
|
|
function updateSelectionCount() {
|
|
const selectedCheckboxes = document.querySelectorAll('.photo-select:checked');
|
|
const count = selectedCheckboxes.length;
|
|
|
|
const selectedCountSpan = document.getElementById('selected-count');
|
|
const downloadBtn = document.getElementById('download-selected-photos');
|
|
const deleteBtn = document.getElementById('delete-selected-photos');
|
|
|
|
if (selectedCountSpan) selectedCountSpan.textContent = `${count} selected`;
|
|
|
|
if (downloadBtn) downloadBtn.disabled = count === 0;
|
|
if (deleteBtn) deleteBtn.disabled = count === 0;
|
|
}
|
|
|
|
// Select all photos
|
|
function selectAllPhotos() {
|
|
const checkboxes = document.querySelectorAll('.photo-select');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = true;
|
|
});
|
|
updateSelectionCount();
|
|
}
|
|
|
|
// Deselect all photos
|
|
function deselectAllPhotos() {
|
|
const checkboxes = document.querySelectorAll('.photo-select');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = false;
|
|
});
|
|
updateSelectionCount();
|
|
}
|
|
|
|
// Download single photo
|
|
function downloadSinglePhoto(index) {
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
|
|
if (index < 0 || index >= capturedPhotos.length) {
|
|
console.error('Invalid photo index:', index);
|
|
return;
|
|
}
|
|
|
|
const photo = capturedPhotos[index];
|
|
|
|
// Handle different photo data structures
|
|
let imageData;
|
|
if (photo.data) {
|
|
// Verification photos store in 'data' property
|
|
imageData = photo.data;
|
|
} else if (photo.dataURL) {
|
|
// Regular photos store in 'dataURL' property
|
|
imageData = photo.dataURL;
|
|
} else if (photo.imageData) {
|
|
// Fallback for other formats
|
|
imageData = photo.imageData;
|
|
} else {
|
|
console.error('Photo missing image data:', photo);
|
|
return;
|
|
}
|
|
|
|
const timestamp = new Date(photo.timestamp || Date.now()).toISOString().slice(0, 19).replace(/:/g, '-');
|
|
const filename = `photo-${timestamp}.png`;
|
|
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = imageData;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Photo downloaded: ${filename}`, 'info');
|
|
}
|
|
console.log(` Downloaded photo: ${filename}`);
|
|
}
|
|
|
|
// Download single verification photo
|
|
function downloadSingleVerificationPhoto(index) {
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
|
|
if (index < 0 || index >= verificationPhotos.length) {
|
|
console.error('Invalid verification photo index:', index);
|
|
return;
|
|
}
|
|
|
|
const photo = verificationPhotos[index];
|
|
const imageData = photo.data || photo.dataUrl; // Verification photos store in 'data'
|
|
|
|
if (!imageData) {
|
|
console.error('Verification photo missing image data:', photo);
|
|
return;
|
|
}
|
|
|
|
const timestamp = new Date(photo.timestamp || Date.now()).toISOString().slice(0, 19).replace(/:/g, '-');
|
|
const phase = photo.phase || 'verification';
|
|
const filename = `verification-${phase}-${timestamp}.png`;
|
|
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = imageData;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Verification photo downloaded: ${filename}`, 'info');
|
|
}
|
|
console.log(` Downloaded verification photo: ${filename}`);
|
|
}
|
|
|
|
// Download selected photos (zip if multiple)
|
|
async function downloadSelectedPhotos() {
|
|
const selectedCheckboxes = document.querySelectorAll('.photo-select:checked');
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
|
|
if (selectedCheckboxes.length === 0) {
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(' No photos selected for download', 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (selectedCheckboxes.length === 1) {
|
|
// Single photo download
|
|
const checkbox = selectedCheckboxes[0];
|
|
const isVerification = checkbox.classList.contains('verification-photo-select');
|
|
|
|
if (isVerification) {
|
|
const verificationIndex = parseInt(checkbox.dataset.verificationIndex);
|
|
downloadSingleVerificationPhoto(verificationIndex);
|
|
} else {
|
|
const index = parseInt(checkbox.dataset.index);
|
|
downloadSinglePhoto(index);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multiple photos - create zip
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(' Creating zip file...', 'info');
|
|
}
|
|
|
|
try {
|
|
// Create zip file (using JSZip if available, otherwise download individually)
|
|
if (typeof JSZip !== 'undefined') {
|
|
const zip = new JSZip();
|
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
|
|
selectedCheckboxes.forEach((checkbox, zipIndex) => {
|
|
const isVerification = checkbox.classList.contains('verification-photo-select');
|
|
let photo, imageData;
|
|
|
|
if (isVerification) {
|
|
// Handle verification photo
|
|
const verificationIndex = parseInt(checkbox.dataset.verificationIndex);
|
|
photo = verificationPhotos[verificationIndex];
|
|
|
|
if (!photo) {
|
|
console.warn(`Verification photo at index ${verificationIndex} not found. Available verification photos:`, verificationPhotos.length);
|
|
return; // Skip this photo
|
|
}
|
|
|
|
imageData = photo.data || photo.dataUrl; // Verification photos store in 'data'
|
|
} else {
|
|
// Handle regular photo
|
|
const index = parseInt(checkbox.dataset.index);
|
|
photo = capturedPhotos[index];
|
|
|
|
if (!photo) {
|
|
console.warn(`Regular photo at index ${index} not found. Available photos:`, capturedPhotos.length);
|
|
return; // Skip this photo
|
|
}
|
|
|
|
imageData = photo.dataURL || photo.data || photo.imageData; // Regular photos store in 'dataURL'
|
|
}
|
|
|
|
if (!imageData) {
|
|
console.warn('Photo missing image data:', photo);
|
|
return; // Skip this photo
|
|
}
|
|
|
|
const photoTimestamp = new Date(photo.timestamp || Date.now()).toISOString().slice(0, 19).replace(/:/g, '-');
|
|
|
|
// Convert base64 to blob
|
|
const base64Data = imageData.split(',')[1];
|
|
zip.file(`photo-${photoTimestamp}-${zipIndex + 1}.png`, base64Data, {base64: true});
|
|
});
|
|
|
|
const zipBlob = await zip.generateAsync({type: 'blob'});
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(zipBlob);
|
|
link.download = `photos-${timestamp}.zip`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Downloaded ${selectedCheckboxes.length} photos as zip file`, 'info');
|
|
}
|
|
} else {
|
|
// Fallback: download individually
|
|
selectedCheckboxes.forEach((checkbox, downloadIndex) => {
|
|
const isVerification = checkbox.classList.contains('verification-photo-select');
|
|
|
|
setTimeout(() => {
|
|
if (isVerification) {
|
|
const verificationIndex = parseInt(checkbox.dataset.verificationIndex);
|
|
downloadSingleVerificationPhoto(verificationIndex);
|
|
} else {
|
|
const index = parseInt(checkbox.dataset.index);
|
|
downloadSinglePhoto(index);
|
|
}
|
|
}, downloadIndex * 100); // Stagger downloads
|
|
});
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Downloading ${selectedCheckboxes.length} photos individually`, 'info');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show('? Error creating download', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete selected photos
|
|
function deleteSelectedPhotos() {
|
|
const selectedCheckboxes = document.querySelectorAll('.photo-select:checked');
|
|
|
|
if (selectedCheckboxes.length === 0) {
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(' No photos selected for deletion', 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const confirmed = confirm(`Are you sure you want to delete ${selectedCheckboxes.length} selected photos?\n\nThis action cannot be undone.`);
|
|
|
|
if (confirmed) {
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
const indicesToDelete = Array.from(selectedCheckboxes).map(cb => parseInt(cb.dataset.index)).sort((a, b) => b - a);
|
|
|
|
// Delete in reverse order to maintain indices
|
|
indicesToDelete.forEach(index => {
|
|
if (index >= 0 && index < capturedPhotos.length) {
|
|
capturedPhotos.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
// Update localStorage
|
|
localStorage.setItem('capturedPhotos', JSON.stringify(capturedPhotos));
|
|
|
|
// Show success message
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(`? Successfully deleted ${indicesToDelete.length} photos!`, 'info');
|
|
}
|
|
|
|
// Refresh the photo galleries
|
|
setupLibraryGalleryTab();
|
|
|
|
console.log(`? Bulk deleted ${indicesToDelete.length} photos`);
|
|
}
|
|
}
|
|
|
|
// Captured Video Functions
|
|
function formatVideoDuration(ms) {
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
// Captured video functions removed - videos now saved directly to user's selected directory
|
|
// Videos can be accessed from the file system at the location chosen in Quick Play setup
|
|
|
|
// Initialize bulk action event listeners
|
|
function initializeBulkActions() {
|
|
const selectAllBtn = document.getElementById('select-all-photos');
|
|
const deselectAllBtn = document.getElementById('deselect-all-photos');
|
|
const downloadSelectedBtn = document.getElementById('download-selected-photos');
|
|
const deleteSelectedBtn = document.getElementById('delete-selected-photos');
|
|
|
|
if (selectAllBtn) selectAllBtn.addEventListener('click', selectAllPhotos);
|
|
if (deselectAllBtn) deselectAllBtn.addEventListener('click', deselectAllPhotos);
|
|
if (downloadSelectedBtn) downloadSelectedBtn.addEventListener('click', downloadSelectedPhotos);
|
|
if (deleteSelectedBtn) deleteSelectedBtn.addEventListener('click', deleteSelectedPhotos);
|
|
}
|
|
|
|
// Verification Photo Functions
|
|
function showVerificationPhotoPreview(imageData, title, message, isStart, timestamp) {
|
|
const photoDate = timestamp ? new Date(timestamp).toLocaleString() : 'Unknown date';
|
|
const photoType = isStart ? ' START Position' : ' END Position';
|
|
|
|
// Create modal overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
overlay.innerHTML = `
|
|
<div style="position: relative; max-width: 90%; max-height: 90%; padding: 20px; background: var(--bg-secondary); border-radius: 10px; border: 2px solid var(--color-error);">
|
|
<div style="text-align: center; margin-bottom: 15px;">
|
|
<h3 style="color: var(--color-error); margin: 0 0 10px 0;">${title}</h3>
|
|
<div style="display: flex; gap: 15px; justify-content: center; margin-bottom: 10px;">
|
|
<span style="color: var(--color-error); font-weight: bold;">${photoType}</span>
|
|
<span style="color: var(--text-muted); font-size: 0.9em;">${photoDate}</span>
|
|
</div>
|
|
<div style="background: var(--bg-primary-overlay-10); border: 1px solid var(--color-error); border-radius: 5px; padding: 10px; margin-bottom: 15px;">
|
|
<h4 style="color: var(--color-error); margin: 0 0 8px 0; font-size: 1.1em;">Degrading Message:</h4>
|
|
<p style="color: var(--color-error); font-style: italic; margin: 0; font-size: 1.1em;">"${message}"</p>
|
|
</div>
|
|
</div>
|
|
<img src="${imageData}" alt="${title}"
|
|
style="width: 100%; height: auto; max-width: 800px; max-height: 600px; object-fit: contain; border-radius: 5px;">
|
|
<button onclick="this.parentElement.parentElement.remove()"
|
|
style="position: absolute; top: 10px; right: 10px; background: var(--color-error); border: none; color: white;
|
|
width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 18px;">?</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) {
|
|
overlay.remove();
|
|
}
|
|
});
|
|
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
function downloadVerificationPhoto(index) {
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
|
|
if (index < 0 || index >= verificationPhotos.length) {
|
|
console.error('Invalid verification photo index:', index);
|
|
return;
|
|
}
|
|
|
|
const photo = verificationPhotos[index];
|
|
const timestamp = new Date(photo.timestamp || Date.now());
|
|
const dateStr = timestamp.toISOString().split('T')[0];
|
|
const timeStr = timestamp.toTimeString().split(' ')[0].replace(/:/g, '-');
|
|
const photoType = photo.phase === 'start' ? 'START' : 'END';
|
|
const imageData = photo.data || photo.dataUrl; // Support both formats
|
|
|
|
if (!imageData) {
|
|
console.error('No image data found for verification photo:', photo);
|
|
return;
|
|
}
|
|
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = imageData;
|
|
link.download = `verification-${photoType}-${dateStr}_${timeStr}.png`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(` Downloaded verification photo: ${photoType}`, 'success');
|
|
}
|
|
}
|
|
|
|
function deleteVerificationPhoto(index) {
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
|
|
if (index < 0 || index >= verificationPhotos.length) {
|
|
console.error('Invalid verification photo index:', index);
|
|
return;
|
|
}
|
|
|
|
const photo = verificationPhotos[index];
|
|
const photoType = photo.phase === 'start' ? 'START' : 'END';
|
|
const confirmed = confirm(`Are you sure you want to delete this ${photoType} verification photo?\n\nThis action cannot be undone.`);
|
|
|
|
if (confirmed) {
|
|
// Remove the photo from the array
|
|
verificationPhotos.splice(index, 1);
|
|
|
|
// Update localStorage
|
|
localStorage.setItem('verificationPhotos', JSON.stringify(verificationPhotos));
|
|
|
|
// Refresh the gallery
|
|
setupLibraryGalleryTab();
|
|
|
|
if (window.game && window.game.flashMessageManager) {
|
|
window.game.flashMessageManager.show(`? Deleted verification photo: ${photoType}`, 'info');
|
|
}
|
|
|
|
console.log(`? Deleted verification photo ${index} (${photoType})`);
|
|
}
|
|
}
|
|
|
|
// Show photo preview in modal
|
|
function showPhotoPreview(imageData, title) {
|
|
// Create modal overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 10000;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
// Create image element
|
|
const img = document.createElement('img');
|
|
img.src = imageData;
|
|
img.alt = title;
|
|
img.style.cssText = `
|
|
max-width: 90%;
|
|
max-height: 90%;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
`;
|
|
|
|
// Create title
|
|
const titleDiv = document.createElement('div');
|
|
titleDiv.textContent = title;
|
|
titleDiv.style.cssText = `
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: white;
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
`;
|
|
|
|
// Add elements to overlay
|
|
overlay.appendChild(img);
|
|
overlay.appendChild(titleDiv);
|
|
|
|
// Close on click
|
|
overlay.addEventListener('click', () => {
|
|
document.body.removeChild(overlay);
|
|
});
|
|
|
|
// Add to page
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
function refreshAllLibraryContent() {
|
|
console.log('Refreshing all library content...');
|
|
|
|
// Refresh based on currently active tab
|
|
const activeTab = document.querySelector('.library-tab.active');
|
|
if (activeTab) {
|
|
const tabType = activeTab.getAttribute('data-tab');
|
|
switch(tabType) {
|
|
case 'images':
|
|
setupLibraryImagesTab();
|
|
break;
|
|
case 'audio':
|
|
setupLibraryAudioTab();
|
|
break;
|
|
case 'video':
|
|
setupLibraryVideoTab();
|
|
break;
|
|
case 'gallery':
|
|
setupLibraryGalleryTab();
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log('Library content refreshed');
|
|
}
|
|
|
|
// Image Directory Management Functions
|
|
function handleAddImageDirectory() {
|
|
console.log('Adding new image directory...');
|
|
|
|
if (window.electronAPI && window.electronAPI.selectDirectory) {
|
|
// Use Electron's dialog to select a directory
|
|
console.log(' Calling electronAPI.selectDirectory...');
|
|
|
|
try {
|
|
const result = window.electronAPI.selectDirectory();
|
|
console.log(' selectDirectory returned:', result, typeof result);
|
|
|
|
// Handle both sync and async results
|
|
if (result && typeof result.then === 'function') {
|
|
// It's a promise
|
|
result.then((directoryResult) => {
|
|
console.log('Directory selection result (async):', directoryResult);
|
|
handleDirectoryResult(directoryResult);
|
|
}).catch(error => {
|
|
console.error('Error selecting image directory (async):', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to select directory', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
// It's synchronous
|
|
console.log('Directory selection result (sync):', result);
|
|
handleDirectoryResult(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error calling selectDirectory:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to open directory dialog', 'error');
|
|
}
|
|
}
|
|
} else {
|
|
console.log('? electronAPI.selectDirectory not available');
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Directory linking is only available in desktop mode', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleDirectoryResult(result) {
|
|
console.log(' Processing directory result:', result);
|
|
|
|
// Handle different possible result structures
|
|
let selectedPath = null;
|
|
|
|
if (result && !result.canceled) {
|
|
// Check for filePaths array (newer Electron API)
|
|
if (result.filePaths && result.filePaths.length > 0) {
|
|
selectedPath = result.filePaths[0];
|
|
}
|
|
// Check for filePath string (older API)
|
|
else if (result.filePath) {
|
|
selectedPath = result.filePath;
|
|
}
|
|
// Check if result is directly a path string
|
|
else if (typeof result === 'string') {
|
|
selectedPath = result;
|
|
}
|
|
// Check for paths array (alternative structure)
|
|
else if (result.paths && result.paths.length > 0) {
|
|
selectedPath = result.paths[0];
|
|
}
|
|
}
|
|
|
|
if (selectedPath) {
|
|
console.log('Selected image directory:', selectedPath);
|
|
// Add directory to linked directories
|
|
addImageDirectory(selectedPath);
|
|
} else {
|
|
console.log('No directory selected or selection was canceled');
|
|
}
|
|
}
|
|
|
|
function handleDirectoryResult(result) {
|
|
console.log(' Processing directory result:', result);
|
|
|
|
// Handle different possible result structures
|
|
let selectedPath = null;
|
|
|
|
if (result && !result.canceled) {
|
|
// Check for filePaths array (newer Electron API)
|
|
if (result.filePaths && result.filePaths.length > 0) {
|
|
selectedPath = result.filePaths[0];
|
|
}
|
|
// Check for filePath string (older API)
|
|
else if (result.filePath) {
|
|
selectedPath = result.filePath;
|
|
}
|
|
// Check if result is directly a path string
|
|
else if (typeof result === 'string') {
|
|
selectedPath = result;
|
|
}
|
|
// Check for paths array (alternative structure)
|
|
else if (result.paths && result.paths.length > 0) {
|
|
selectedPath = result.paths[0];
|
|
}
|
|
}
|
|
|
|
if (selectedPath) {
|
|
console.log('Selected image directory:', selectedPath);
|
|
// Add directory to linked directories
|
|
addImageDirectory(selectedPath);
|
|
} else {
|
|
console.log('No directory selected or selection was canceled');
|
|
}
|
|
}
|
|
|
|
function addImageDirectory(directoryPath) {
|
|
// Get existing linked directories
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
|
|
// Check if directory is already linked
|
|
if (linkedDirs.some(dir => dir.path === directoryPath)) {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Directory already linked', 'warning');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Add new directory
|
|
const newDir = {
|
|
id: Date.now().toString(),
|
|
path: directoryPath,
|
|
name: directoryPath.split('\\').pop() || directoryPath.split('/').pop(),
|
|
addedAt: new Date().toISOString()
|
|
};
|
|
|
|
linkedDirs.push(newDir);
|
|
localStorage.setItem('linkedImageDirectories', JSON.stringify(linkedDirs));
|
|
|
|
console.log('Added image directory:', newDir);
|
|
|
|
// Refresh the display
|
|
updateImageDirectoriesList();
|
|
loadLinkedImages();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? Added image directory: ${newDir.name}`, 'success');
|
|
}
|
|
}
|
|
|
|
// Individual Images Management Functions
|
|
function handleAddIndividualImages() {
|
|
console.log('Adding individual images...');
|
|
|
|
if (window.electronAPI && window.electronAPI.selectImages) {
|
|
// Use Electron's dialog to select multiple images
|
|
console.log(' Calling electronAPI.selectImages...');
|
|
|
|
try {
|
|
const result = window.electronAPI.selectImages();
|
|
console.log(' selectImages returned:', result, typeof result);
|
|
|
|
// Handle both sync and async results
|
|
if (result && typeof result.then === 'function') {
|
|
// It's a promise
|
|
result.then((imageResult) => {
|
|
console.log('Individual images selection result (async):', imageResult);
|
|
handleIndividualImagesResult(imageResult);
|
|
}).catch(error => {
|
|
console.error('Error selecting individual images (async):', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to select images', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
// It's synchronous
|
|
console.log('Individual images selection result (sync):', result);
|
|
handleIndividualImagesResult(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error calling selectImages:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to open image selection dialog', 'error');
|
|
}
|
|
}
|
|
} else {
|
|
console.log('? electronAPI.selectImages not available');
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Individual image selection is only available in desktop mode', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleIndividualImagesResult(result) {
|
|
console.log(' Processing individual images result:', result);
|
|
|
|
let selectedPaths = [];
|
|
|
|
if (result && !result.canceled) {
|
|
// Check for filePaths array (newer Electron API)
|
|
if (result.filePaths && result.filePaths.length > 0) {
|
|
selectedPaths = result.filePaths;
|
|
}
|
|
// Check for paths array (alternative structure)
|
|
else if (result.paths && result.paths.length > 0) {
|
|
selectedPaths = result.paths;
|
|
}
|
|
// Check if result is directly an array of paths
|
|
else if (Array.isArray(result)) {
|
|
selectedPaths = result;
|
|
}
|
|
}
|
|
|
|
if (selectedPaths.length > 0) {
|
|
console.log(`Selected ${selectedPaths.length} individual images:`, selectedPaths);
|
|
// Add individual images to collection
|
|
addIndividualImages(selectedPaths);
|
|
} else {
|
|
console.log('No images selected or selection was canceled');
|
|
}
|
|
}
|
|
|
|
function addIndividualImages(imagePaths) {
|
|
// Get existing individual images
|
|
const individualImages = JSON.parse(localStorage.getItem('linkedIndividualImages') || '[]');
|
|
|
|
let addedCount = 0;
|
|
imagePaths.forEach(imagePath => {
|
|
// Check if image is already linked
|
|
if (!individualImages.some(img => img.path === imagePath)) {
|
|
const newImage = {
|
|
id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9),
|
|
path: imagePath,
|
|
name: imagePath.split('\\').pop() || imagePath.split('/').pop(),
|
|
addedAt: new Date().toISOString(),
|
|
type: 'individual'
|
|
};
|
|
|
|
individualImages.push(newImage);
|
|
addedCount++;
|
|
console.log('Added individual image:', newImage);
|
|
}
|
|
});
|
|
|
|
if (addedCount > 0) {
|
|
localStorage.setItem('linkedIndividualImages', JSON.stringify(individualImages));
|
|
|
|
// Refresh the display
|
|
updateImageDirectoriesList();
|
|
loadLinkedImages();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? Added ${addedCount} individual image(s)`, 'success');
|
|
}
|
|
} else {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('All selected images were already linked', 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleRefreshImageDirectories() {
|
|
console.log('Refreshing image directories...');
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(' Refreshing image directories...', 'info');
|
|
}
|
|
|
|
// Reload images from all linked directories
|
|
loadLinkedImages();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Image directories refreshed!', 'success');
|
|
}
|
|
}
|
|
|
|
function handleClearImageDirectories() {
|
|
if (!confirm('Are you sure you want to unlink all image directories and individual images? This will not delete your actual image files.')) {
|
|
return;
|
|
}
|
|
|
|
console.log('Clearing all image directories and individual images...');
|
|
|
|
// Clear from localStorage
|
|
localStorage.removeItem('linkedImageDirectories');
|
|
localStorage.removeItem('linkedIndividualImages');
|
|
|
|
// Update display
|
|
updateImageDirectoriesList();
|
|
loadLinkedImages();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? All image directories and individual images unlinked', 'info');
|
|
}
|
|
}
|
|
|
|
function removeImageDirectory(directoryId) {
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
const updatedDirs = linkedDirs.filter(dir => dir.id !== directoryId);
|
|
|
|
localStorage.setItem('linkedImageDirectories', JSON.stringify(updatedDirs));
|
|
|
|
// Update display
|
|
updateImageDirectoriesList();
|
|
loadLinkedImages();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Image directory unlinked', 'info');
|
|
}
|
|
}
|
|
|
|
function updateImageDirectoriesList() {
|
|
const listContainer = document.getElementById('linked-image-directories-list');
|
|
if (!listContainer) return;
|
|
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
|
|
if (linkedDirs.length === 0) {
|
|
listContainer.innerHTML = '<div class="no-directories">No image directories linked yet</div>';
|
|
return;
|
|
}
|
|
|
|
listContainer.innerHTML = linkedDirs.map(dir => `
|
|
<div class="directory-item">
|
|
<div class="directory-info">
|
|
<div class="directory-name">${dir.name}</div>
|
|
<div class="directory-path">${dir.path}</div>
|
|
</div>
|
|
<button class="btn btn-small btn-danger" onclick="removeImageDirectory('${dir.id}')">
|
|
? Remove
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async function loadLinkedImages() {
|
|
const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
|
|
let allImages = [];
|
|
|
|
console.log(` Loading images from ${linkedDirs.length} linked directories...`);
|
|
console.log(' Linked directories:', linkedDirs.map(d => d.path));
|
|
|
|
if (window.electronAPI && linkedDirs.length > 0) {
|
|
// Scan each linked directory for images
|
|
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i;
|
|
|
|
for (const dir of linkedDirs) {
|
|
console.log(` Scanning directory: ${dir.path} (${dir.name})`);
|
|
try {
|
|
if (window.electronAPI.readDirectory) {
|
|
const filesPromise = window.electronAPI.readDirectory(dir.path);
|
|
console.log(` ReadDirectory result for ${dir.path}:`, filesPromise);
|
|
|
|
// Handle async result
|
|
let files = [];
|
|
if (filesPromise && typeof filesPromise.then === 'function') {
|
|
files = await filesPromise;
|
|
console.log(` Resolved files in ${dir.path}:`, files.length, 'files');
|
|
} else if (Array.isArray(filesPromise)) {
|
|
files = filesPromise;
|
|
console.log(` Sync files in ${dir.path}:`, files.length, 'files');
|
|
}
|
|
|
|
if (files && files.length > 0) {
|
|
// Filter for image files
|
|
const imageFiles = files.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return imageExtensions.test(fileName);
|
|
});
|
|
|
|
console.log(` Found ${imageFiles.length} image files in ${dir.path}`);
|
|
|
|
if (imageFiles.length > 0) {
|
|
const dirImages = imageFiles.map(file => {
|
|
if (typeof file === 'object' && file.name && file.path) {
|
|
// Already has name and path
|
|
return {
|
|
path: file.path,
|
|
name: file.name,
|
|
directory: dir.name,
|
|
directoryId: dir.id
|
|
};
|
|
} else {
|
|
// Just a filename string
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
const fullPath = window.electronAPI.pathJoin ? window.electronAPI.pathJoin(dir.path, fileName) : `${dir.path}\\${fileName}`;
|
|
return {
|
|
path: fullPath,
|
|
name: fileName,
|
|
directory: dir.name,
|
|
directoryId: dir.id
|
|
};
|
|
}
|
|
});
|
|
allImages = allImages.concat(dirImages);
|
|
console.log(` Added ${dirImages.length} images from ${dir.name} to gallery`);
|
|
}
|
|
} else {
|
|
console.log(` No files found in ${dir.path}`);
|
|
}
|
|
} else {
|
|
console.log(' electronAPI.readDirectory not available');
|
|
}
|
|
} catch (error) {
|
|
console.error(` Error scanning directory ${dir.path}:`, error);
|
|
}
|
|
}
|
|
} else if (linkedDirs.length === 0) {
|
|
console.log(' No linked directories found in localStorage');
|
|
} else {
|
|
console.log(' electronAPI not available for directory scanning');
|
|
}
|
|
|
|
// Load individual images
|
|
const individualImages = JSON.parse(localStorage.getItem('linkedIndividualImages') || '[]');
|
|
if (individualImages.length > 0) {
|
|
console.log(` Loading ${individualImages.length} individual images...`);
|
|
|
|
individualImages.forEach(image => {
|
|
// Verify the image still exists and add to gallery
|
|
allImages.push({
|
|
path: image.path,
|
|
name: image.name,
|
|
directory: 'Individual Images',
|
|
directoryId: 'individual',
|
|
type: 'individual'
|
|
});
|
|
});
|
|
|
|
console.log(` Added ${individualImages.length} individual images to gallery`);
|
|
} else {
|
|
console.log(' No individual images found in localStorage');
|
|
}
|
|
|
|
// Load verification photos from localStorage
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
if (verificationPhotos.length > 0) {
|
|
console.log(` Loading ${verificationPhotos.length} verification photos...`);
|
|
|
|
verificationPhotos.forEach((photo, index) => {
|
|
// Add verification photo to gallery with special metadata
|
|
const imageData = photo.data || photo.dataUrl; // Support both formats
|
|
if (imageData) {
|
|
allImages.push({
|
|
path: imageData,
|
|
name: `Verification Photo ${index + 1}`,
|
|
directory: 'Verification Photos',
|
|
directoryId: 'verification',
|
|
type: 'verification',
|
|
timestamp: photo.timestamp,
|
|
message: photo.message,
|
|
isStart: photo.phase === 'start'
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log(` Added ${verificationPhotos.length} verification photos to gallery`);
|
|
} else {
|
|
console.log(' No verification photos found in localStorage');
|
|
}
|
|
|
|
console.log(` Total images found: ${allImages.length}`);
|
|
|
|
// Update the image count display
|
|
const imageCountElement = document.getElementById('lib-image-count');
|
|
if (imageCountElement) {
|
|
imageCountElement.textContent = `${allImages.length} images`;
|
|
}
|
|
|
|
// Update the directories count display
|
|
const dirCountElement = document.getElementById('lib-directories-count');
|
|
if (dirCountElement) {
|
|
dirCountElement.textContent = `${linkedDirs.length} directories linked`;
|
|
}
|
|
|
|
// Store all images globally for filtering
|
|
window.allLinkedImages = allImages;
|
|
|
|
// Apply current filter and populate the gallery
|
|
const categoryFilter = document.getElementById('lib-image-category-filter');
|
|
const selectedCategory = categoryFilter ? categoryFilter.value : 'all';
|
|
const filteredImages = filterImagesByCategory(allImages, selectedCategory);
|
|
populateImageGallery(filteredImages);
|
|
|
|
// Update count display
|
|
const countElement = document.getElementById('lib-image-count');
|
|
if (countElement) {
|
|
countElement.textContent = `${filteredImages.length} images`;
|
|
}
|
|
}
|
|
|
|
function filterImagesByCategory(images, category) {
|
|
if (category === 'all') {
|
|
return images;
|
|
}
|
|
|
|
// File type filtering
|
|
if (category === 'jpg' || category === 'png' || category === 'gif') {
|
|
return images.filter(image => {
|
|
const extension = image.path.toLowerCase().split('.').pop();
|
|
if (category === 'jpg') {
|
|
return extension === 'jpg' || extension === 'jpeg';
|
|
}
|
|
return extension === category;
|
|
});
|
|
}
|
|
|
|
// Category filtering based on directory names
|
|
return images.filter(image => {
|
|
const dirName = image.directory.toLowerCase();
|
|
switch (category) {
|
|
case 'tasks':
|
|
return dirName.includes('task');
|
|
case 'consequences':
|
|
return dirName.includes('consequence');
|
|
case 'rewards':
|
|
return dirName.includes('reward');
|
|
case 'verification':
|
|
return image.type === 'verification';
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
function populateImageGallery(images) {
|
|
const imageGallery = document.getElementById('lib-image-gallery');
|
|
if (!imageGallery) {
|
|
console.error('? lib-image-gallery element not found!');
|
|
return;
|
|
}
|
|
|
|
console.log(` Populating gallery with ${images.length} images`);
|
|
console.log(' Gallery element:', imageGallery);
|
|
|
|
if (images.length === 0) {
|
|
imageGallery.innerHTML = `
|
|
<div class="no-images-message">
|
|
<p>No images found in linked directories</p>
|
|
<p>Click "Add Directory" to link a folder containing images</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Clear existing content
|
|
imageGallery.innerHTML = '';
|
|
console.log(' Cleared existing gallery content');
|
|
|
|
// Create image grid
|
|
images.forEach((image, index) => {
|
|
const imgElement = document.createElement('div');
|
|
imgElement.className = 'gallery-item image-item';
|
|
|
|
// Special formatting for verification photos
|
|
if (image.type === 'verification') {
|
|
const photoDate = image.timestamp ? new Date(image.timestamp).toLocaleString() : 'Unknown';
|
|
const photoType = image.isStart ? ' START' : ' END';
|
|
const degradingMessage = image.message || 'Position verification photo';
|
|
|
|
imgElement.innerHTML = `
|
|
<img src="${image.path}" alt="${image.name}" loading="lazy" />
|
|
<div class="image-info verification-photo-info">
|
|
<div class="image-name">${image.name}</div>
|
|
<div class="verification-type">${photoType} Photo</div>
|
|
<div class="verification-message">"${degradingMessage}"</div>
|
|
<div class="verification-timestamp">${photoDate}</div>
|
|
<div class="image-directory">${image.directory}</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// Standard formatting for regular images
|
|
imgElement.innerHTML = `
|
|
<img src="${image.path}" alt="${image.name}" loading="lazy" />
|
|
<div class="image-info">
|
|
<div class="image-name">${image.name}</div>
|
|
<div class="image-directory">${image.directory}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add click handler for image preview
|
|
imgElement.addEventListener('click', function() {
|
|
if (image.type === 'verification') {
|
|
// Enhanced preview for verification photos with message
|
|
previewVerificationImage(image.path, image.name, image.message, image.isStart, image.timestamp);
|
|
} else {
|
|
previewImage(image.path, image.name);
|
|
}
|
|
});
|
|
|
|
imageGallery.appendChild(imgElement);
|
|
|
|
if (index < 5) {
|
|
console.log(` Added image ${index + 1}: ${image.name}`);
|
|
}
|
|
});
|
|
|
|
console.log(`? Created ${images.length} linked image gallery items`);
|
|
console.log(' Gallery innerHTML length:', imageGallery.innerHTML.length);
|
|
}
|
|
|
|
function previewImage(imageUrl, imageName) {
|
|
// Create or show image preview modal
|
|
let modal = document.getElementById('image-preview-modal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'image-preview-modal';
|
|
modal.className = 'image-preview-modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-backdrop" onclick="closeImagePreview()"></div>
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<span id="preview-image-name">${imageName}</span>
|
|
<button class="close-btn" onclick="closeImagePreview()"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<img id="preview-image" src="${imageUrl}" alt="${imageName}" />
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
} else {
|
|
document.getElementById('preview-image-name').textContent = imageName;
|
|
document.getElementById('preview-image').src = imageUrl;
|
|
document.getElementById('preview-image').alt = imageName;
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
}
|
|
|
|
function previewVerificationImage(imageUrl, imageName, message, isStart, timestamp) {
|
|
// Create or show enhanced verification image preview modal
|
|
let modal = document.getElementById('image-preview-modal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'image-preview-modal';
|
|
modal.className = 'image-preview-modal';
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
const photoDate = timestamp ? new Date(timestamp).toLocaleString() : 'Unknown date';
|
|
const photoType = isStart ? ' START Position' : ' END Position';
|
|
const degradingMessage = message || 'Position verification photo';
|
|
|
|
modal.innerHTML = `
|
|
<div class="modal-backdrop" onclick="closeImagePreview()"></div>
|
|
<div class="modal-content verification-modal">
|
|
<div class="modal-header">
|
|
<div class="verification-header">
|
|
<span id="preview-image-name">${imageName}</span>
|
|
<div class="verification-details">
|
|
<span class="verification-type">${photoType}</span>
|
|
<span class="verification-timestamp">${photoDate}</span>
|
|
</div>
|
|
</div>
|
|
<button class="close-btn" onclick="closeImagePreview()"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<img id="preview-image" src="${imageUrl}" alt="${imageName}" />
|
|
<div class="verification-message-display">
|
|
<h4>Degrading Message:</h4>
|
|
<p>"${degradingMessage}"</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
}
|
|
|
|
function closeImagePreview() {
|
|
const modal = document.getElementById('image-preview-modal');
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
document.body.style.overflow = ''; // Restore scrolling
|
|
}
|
|
}
|
|
|
|
// Global functions for HTML onclick handlers
|
|
window.removeImageDirectory = removeImageDirectory;
|
|
window.closeImagePreview = closeImagePreview;
|
|
|
|
// Video Directory Management Functions
|
|
function handleAddVideoDirectory() {
|
|
console.log('Adding new video directory...');
|
|
|
|
if (window.electronAPI && window.electronAPI.selectDirectory) {
|
|
console.log(' Calling electronAPI.selectDirectory...');
|
|
|
|
try {
|
|
const result = window.electronAPI.selectDirectory();
|
|
console.log(' selectDirectory returned:', result, typeof result);
|
|
|
|
if (result && typeof result.then === 'function') {
|
|
result.then((directoryResult) => {
|
|
console.log('Video directory selection result (async):', directoryResult);
|
|
handleVideoDirectoryResult(directoryResult);
|
|
}).catch(error => {
|
|
console.error('Error selecting video directory (async):', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to select directory', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Video directory selection result (sync):', result);
|
|
handleVideoDirectoryResult(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error calling selectDirectory:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to open directory selection dialog', 'error');
|
|
}
|
|
}
|
|
} else {
|
|
console.log('? electronAPI.selectDirectory not available');
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Directory selection is only available in desktop mode', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleVideoDirectoryResult(result) {
|
|
console.log(' Processing video directory result:', result);
|
|
|
|
let selectedPath = null;
|
|
|
|
if (result && !result.canceled) {
|
|
if (result.filePaths && result.filePaths.length > 0) {
|
|
selectedPath = result.filePaths[0];
|
|
} else if (typeof result === 'string') {
|
|
selectedPath = result;
|
|
} else if (result.path) {
|
|
selectedPath = result.path;
|
|
}
|
|
|
|
if (selectedPath) {
|
|
console.log('Selected video directory:', selectedPath);
|
|
addVideoDirectory(selectedPath);
|
|
} else {
|
|
console.log('No valid path found in result:', result);
|
|
}
|
|
} else {
|
|
console.log('Video directory selection was canceled');
|
|
}
|
|
}
|
|
|
|
function addVideoDirectory(directoryPath) {
|
|
let linkedDirs;
|
|
try {
|
|
linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedDirs)) {
|
|
linkedDirs = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedVideoDirectories, resetting to empty array:', e);
|
|
linkedDirs = [];
|
|
}
|
|
|
|
if (linkedDirs.some(dir => dir.path === directoryPath)) {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Directory is already linked', 'info');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const newDirectory = {
|
|
id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9),
|
|
path: directoryPath,
|
|
name: directoryPath.split('\\').pop() || directoryPath.split('/').pop(),
|
|
addedAt: new Date().toISOString()
|
|
};
|
|
|
|
linkedDirs.push(newDirectory);
|
|
localStorage.setItem('linkedVideoDirectories', JSON.stringify(linkedDirs));
|
|
|
|
updateVideoDirectoriesList();
|
|
loadLinkedVideos();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? Added video directory: ${newDirectory.name}`, 'success');
|
|
}
|
|
}
|
|
|
|
function handleAddIndividualVideos() {
|
|
console.log('Adding individual videos...');
|
|
|
|
if (window.electronAPI && window.electronAPI.selectVideos) {
|
|
console.log(' Calling electronAPI.selectVideos...');
|
|
|
|
try {
|
|
const result = window.electronAPI.selectVideos();
|
|
console.log(' selectVideos returned:', result, typeof result);
|
|
|
|
if (result && typeof result.then === 'function') {
|
|
result.then((videoResult) => {
|
|
console.log('Individual videos selection result (async):', videoResult);
|
|
handleIndividualVideosResult(videoResult);
|
|
}).catch(error => {
|
|
console.error('Error selecting individual videos (async):', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to select videos', 'error');
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Individual videos selection result (sync):', result);
|
|
handleIndividualVideosResult(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error calling selectVideos:', error);
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Failed to open video selection dialog', 'error');
|
|
}
|
|
}
|
|
} else {
|
|
console.log('? electronAPI.selectVideos not available');
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('Individual video selection is only available in desktop mode', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleIndividualVideosResult(result) {
|
|
console.log(' Processing individual videos result:', result);
|
|
|
|
let selectedPaths = [];
|
|
|
|
if (result && !result.canceled) {
|
|
if (result.filePaths && result.filePaths.length > 0) {
|
|
selectedPaths = result.filePaths;
|
|
} else if (Array.isArray(result)) {
|
|
selectedPaths = result;
|
|
} else if (typeof result === 'string') {
|
|
selectedPaths = [result];
|
|
}
|
|
|
|
console.log(`Selected ${selectedPaths.length} individual videos:`, selectedPaths);
|
|
addIndividualVideos(selectedPaths);
|
|
} else {
|
|
console.log('No videos selected or selection was canceled');
|
|
}
|
|
}
|
|
|
|
function addIndividualVideos(videoPaths) {
|
|
let individualVideos;
|
|
try {
|
|
individualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]');
|
|
if (!Array.isArray(individualVideos)) {
|
|
individualVideos = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedIndividualVideos, resetting to empty array:', e);
|
|
individualVideos = [];
|
|
}
|
|
|
|
let addedCount = 0;
|
|
videoPaths.forEach(videoPath => {
|
|
if (!individualVideos.some(vid => vid.path === videoPath)) {
|
|
const newVideo = {
|
|
id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9),
|
|
path: videoPath,
|
|
name: videoPath.split('\\').pop() || videoPath.split('/').pop(),
|
|
addedAt: new Date().toISOString(),
|
|
type: 'individual'
|
|
};
|
|
|
|
individualVideos.push(newVideo);
|
|
addedCount++;
|
|
console.log('Added individual video:', newVideo);
|
|
}
|
|
});
|
|
|
|
if (addedCount > 0) {
|
|
localStorage.setItem('linkedIndividualVideos', JSON.stringify(individualVideos));
|
|
|
|
updateVideoDirectoriesList();
|
|
loadLinkedVideos();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(`? Added ${addedCount} individual video(s)`, 'success');
|
|
}
|
|
} else {
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('All selected videos were already linked', 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleRefreshVideoDirectories() {
|
|
console.log('Refreshing video directories...');
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification(' Refreshing video directories...', 'info');
|
|
}
|
|
|
|
loadLinkedVideos();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Video directories refreshed!', 'success');
|
|
}
|
|
}
|
|
|
|
function handleClearVideoDirectories() {
|
|
if (!confirm('Are you sure you want to unlink all video directories and individual videos? This will not delete your actual video files.')) {
|
|
return;
|
|
}
|
|
|
|
console.log('Clearing all video directories and individual videos...');
|
|
|
|
localStorage.removeItem('linkedVideoDirectories');
|
|
localStorage.removeItem('linkedIndividualVideos');
|
|
|
|
updateVideoDirectoriesList();
|
|
loadLinkedVideos();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? All video directories and individual videos unlinked', 'info');
|
|
}
|
|
}
|
|
|
|
function removeVideoDirectory(directoryId) {
|
|
let linkedDirs;
|
|
try {
|
|
linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedDirs)) {
|
|
linkedDirs = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedVideoDirectories, resetting to empty array:', e);
|
|
linkedDirs = [];
|
|
}
|
|
|
|
const updatedDirs = linkedDirs.filter(dir => dir.id !== directoryId);
|
|
|
|
localStorage.setItem('linkedVideoDirectories', JSON.stringify(updatedDirs));
|
|
|
|
updateVideoDirectoriesList();
|
|
loadLinkedVideos();
|
|
|
|
if (window.game && window.game.showNotification) {
|
|
window.game.showNotification('? Video directory unlinked', 'info');
|
|
}
|
|
}
|
|
|
|
function updateVideoDirectoriesList() {
|
|
const listContainer = document.getElementById('linked-video-directories-list');
|
|
if (!listContainer) return;
|
|
|
|
let linkedDirs;
|
|
let individualVideos;
|
|
try {
|
|
linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedDirs)) {
|
|
linkedDirs = [];
|
|
}
|
|
individualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]');
|
|
if (!Array.isArray(individualVideos)) {
|
|
individualVideos = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing video directories, resetting to empty arrays:', e);
|
|
linkedDirs = [];
|
|
individualVideos = [];
|
|
}
|
|
|
|
const dirCountElement = document.getElementById('lib-video-directories-count');
|
|
if (dirCountElement) {
|
|
dirCountElement.textContent = `${linkedDirs.length} directories linked`;
|
|
}
|
|
|
|
if (linkedDirs.length === 0 && individualVideos.length === 0) {
|
|
listContainer.innerHTML = '<div class="no-directories">No video directories linked yet</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
linkedDirs.forEach(dir => {
|
|
html += `
|
|
<div class="directory-item">
|
|
<div class="directory-info">
|
|
<div class="directory-name"> ${dir.name}</div>
|
|
<div class="directory-path">${dir.path}</div>
|
|
<div class="directory-meta">Added ${new Date(dir.addedAt).toLocaleDateString()}</div>
|
|
</div>
|
|
<button class="remove-directory-btn" onclick="removeVideoDirectory('${dir.id}')">?</button>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (individualVideos.length > 0) {
|
|
html += `
|
|
<div class="directory-item individual-videos">
|
|
<div class="directory-info">
|
|
<div class="directory-name"> Individual Videos</div>
|
|
<div class="directory-path">${individualVideos.length} video(s) selected individually</div>
|
|
<div class="directory-meta">Click "Add Individual Videos" to add more</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
listContainer.innerHTML = html;
|
|
}
|
|
|
|
async function loadLinkedVideos() {
|
|
console.log(' Loading linked videos...');
|
|
|
|
let linkedDirs;
|
|
try {
|
|
linkedDirs = JSON.parse(localStorage.getItem('linkedVideoDirectories') || '[]');
|
|
if (!Array.isArray(linkedDirs)) {
|
|
linkedDirs = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedVideoDirectories, resetting to empty array:', e);
|
|
linkedDirs = [];
|
|
}
|
|
|
|
const allVideos = [];
|
|
|
|
if (window.electronAPI && window.electronAPI.readDirectory) {
|
|
const videoExtensions = /\.(mp4|webm|avi|mov|mkv|wmv|flv|m4v)$/i;
|
|
|
|
for (const dir of linkedDirs) {
|
|
try {
|
|
console.log(` Scanning directory: ${dir.path}`);
|
|
console.log(` electronAPI available:`, !!window.electronAPI);
|
|
console.log(` readDirectory available:`, !!window.electronAPI.readDirectory);
|
|
|
|
if (!window.electronAPI.readDirectory) {
|
|
console.error(`? readDirectory function not available`);
|
|
continue;
|
|
}
|
|
|
|
// Try video-specific directory reading first
|
|
let filesPromise;
|
|
if (window.electronAPI.readVideoDirectory) {
|
|
console.log(` Using readVideoDirectory for ${dir.path}`);
|
|
filesPromise = window.electronAPI.readVideoDirectory(dir.path);
|
|
} else if (window.electronAPI.readVideoDirectoryRecursive) {
|
|
console.log(` Using readVideoDirectoryRecursive for ${dir.path}`);
|
|
filesPromise = window.electronAPI.readVideoDirectoryRecursive(dir.path);
|
|
} else {
|
|
console.log(` Using generic readDirectory for ${dir.path}`);
|
|
filesPromise = window.electronAPI.readDirectory(dir.path);
|
|
}
|
|
console.log(` ReadDirectory result for ${dir.path}:`, filesPromise);
|
|
console.log(` Result type:`, typeof filesPromise);
|
|
console.log(` Is promise:`, filesPromise && typeof filesPromise.then === 'function');
|
|
console.log(` Is array:`, Array.isArray(filesPromise));
|
|
|
|
// Handle async result
|
|
let files = [];
|
|
if (filesPromise && typeof filesPromise.then === 'function') {
|
|
try {
|
|
files = await filesPromise;
|
|
console.log(` Resolved files in ${dir.path}:`, files);
|
|
console.log(` Files length:`, files ? files.length : 'null/undefined');
|
|
console.log(` Files type:`, typeof files);
|
|
} catch (promiseError) {
|
|
console.error(`? Error resolving promise for ${dir.path}:`, promiseError);
|
|
continue;
|
|
}
|
|
} else if (Array.isArray(filesPromise)) {
|
|
files = filesPromise;
|
|
console.log(` Sync files in ${dir.path}:`, files.length, 'files');
|
|
} else {
|
|
console.error(`? Unexpected readDirectory result type for ${dir.path}:`, typeof filesPromise, filesPromise);
|
|
continue;
|
|
}
|
|
|
|
// If we got an empty array, try alternative methods
|
|
if (!files || files.length === 0) {
|
|
console.log(` Empty result from readDirectory, trying alternative approaches...`);
|
|
|
|
// Try with different path formats
|
|
const normalizedPath = dir.path.replace(/\//g, '\\');
|
|
console.log(` Trying normalized path: ${normalizedPath}`);
|
|
|
|
if (normalizedPath !== dir.path) {
|
|
try {
|
|
const altResult = await window.electronAPI.readDirectory(normalizedPath);
|
|
console.log(` Alternative path result:`, altResult);
|
|
if (altResult && altResult.length > 0) {
|
|
files = altResult;
|
|
console.log(` Success with normalized path! Found ${files.length} files`);
|
|
}
|
|
} catch (altError) {
|
|
console.log(` Alternative path also failed:`, altError);
|
|
}
|
|
}
|
|
|
|
// If still empty, check if there are other electron APIs we can use
|
|
if (!files || files.length === 0) {
|
|
console.log(` Available electronAPI methods:`, Object.keys(window.electronAPI));
|
|
|
|
// Try to list files using different methods if available
|
|
if (window.electronAPI.readVideoDirectory) {
|
|
try {
|
|
const videoResult = await window.electronAPI.readVideoDirectory(dir.path);
|
|
console.log(` readVideoDirectory result:`, videoResult);
|
|
if (videoResult && videoResult.length > 0) {
|
|
files = videoResult;
|
|
console.log(` Success with readVideoDirectory! Found ${files.length} files`);
|
|
}
|
|
} catch (videoError) {
|
|
console.log(` readVideoDirectory also failed:`, videoError);
|
|
}
|
|
}
|
|
|
|
if ((!files || files.length === 0) && window.electronAPI.readVideoDirectoryRecursive) {
|
|
try {
|
|
const recursiveResult = await window.electronAPI.readVideoDirectoryRecursive(dir.path);
|
|
console.log(` readVideoDirectoryRecursive result:`, recursiveResult);
|
|
if (recursiveResult && recursiveResult.length > 0) {
|
|
files = recursiveResult;
|
|
console.log(` Success with readVideoDirectoryRecursive! Found ${files.length} files`);
|
|
}
|
|
} catch (recursiveError) {
|
|
console.log(` readVideoDirectoryRecursive also failed:`, recursiveError);
|
|
}
|
|
}
|
|
|
|
if (window.electronAPI.listFiles) {
|
|
try {
|
|
const listResult = await window.electronAPI.listFiles(dir.path);
|
|
console.log(` listFiles result:`, listResult);
|
|
if (listResult && listResult.length > 0) {
|
|
files = listResult;
|
|
console.log(` Success with listFiles! Found ${files.length} files`);
|
|
}
|
|
} catch (listError) {
|
|
console.log(` listFiles also failed:`, listError);
|
|
}
|
|
}
|
|
|
|
if (window.electronAPI.scanDirectory) {
|
|
try {
|
|
const scanResult = await window.electronAPI.scanDirectory(dir.path);
|
|
console.log(` scanDirectory result:`, scanResult);
|
|
if (scanResult && scanResult.length > 0) {
|
|
files = scanResult;
|
|
console.log(` Success with scanDirectory! Found ${files.length} files`);
|
|
}
|
|
} catch (scanError) {
|
|
console.log(` scanDirectory also failed:`, scanError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (files && files.length > 0) {
|
|
console.log(` Sample files:`, files.slice(0, 5));
|
|
|
|
// Filter for video files
|
|
const videoFiles = files.filter(file => {
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
return videoExtensions.test(fileName);
|
|
});
|
|
|
|
console.log(` Found ${videoFiles.length} video files in ${dir.name}`);
|
|
console.log(` Sample video files:`, videoFiles.slice(0, 5));
|
|
|
|
if (videoFiles.length > 0) {
|
|
const dirVideos = videoFiles.map(file => {
|
|
if (typeof file === 'object' && file.name && file.path) {
|
|
// Already has name and path
|
|
return {
|
|
path: file.path,
|
|
name: file.name,
|
|
directory: dir.name,
|
|
directoryId: dir.id,
|
|
type: 'directory'
|
|
};
|
|
} else {
|
|
// Just a filename string
|
|
const fileName = typeof file === 'object' ? file.name : file;
|
|
const fullPath = window.electronAPI.pathJoin ? window.electronAPI.pathJoin(dir.path, fileName) : `${dir.path}\\${fileName}`;
|
|
return {
|
|
path: fullPath,
|
|
name: fileName,
|
|
directory: dir.name,
|
|
directoryId: dir.id,
|
|
type: 'directory'
|
|
};
|
|
}
|
|
});
|
|
|
|
allVideos.push(...dirVideos);
|
|
console.log(` Added ${dirVideos.length} videos from ${dir.name}`);
|
|
}
|
|
} else {
|
|
console.log(` No files found in ${dir.path}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error reading video directory ${dir.path}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
let individualVideos;
|
|
try {
|
|
individualVideos = JSON.parse(localStorage.getItem('linkedIndividualVideos') || '[]');
|
|
if (!Array.isArray(individualVideos)) {
|
|
individualVideos = [];
|
|
}
|
|
} catch (e) {
|
|
console.log('Error parsing linkedIndividualVideos, resetting to empty array:', e);
|
|
individualVideos = [];
|
|
}
|
|
|
|
if (individualVideos.length > 0) {
|
|
console.log(` Loading ${individualVideos.length} individual videos...`);
|
|
|
|
individualVideos.forEach(video => {
|
|
allVideos.push({
|
|
path: video.path,
|
|
name: video.name,
|
|
directory: 'Individual Videos',
|
|
directoryId: 'individual',
|
|
type: 'individual'
|
|
});
|
|
});
|
|
|
|
console.log(` Added ${individualVideos.length} individual videos to gallery`);
|
|
}
|
|
|
|
console.log(` Total videos found: ${allVideos.length}`);
|
|
|
|
// Include captured videos in count
|
|
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
|
const totalVideos = allVideos.length + capturedVideos.length;
|
|
|
|
const videoCountElement = document.getElementById('lib-video-count');
|
|
if (videoCountElement) {
|
|
videoCountElement.textContent = `${totalVideos} videos`;
|
|
console.log(` Updated video count: ${totalVideos} (${allVideos.length} linked + ${capturedVideos.length} captured)`);
|
|
}
|
|
|
|
await populateVideoGallery(allVideos);
|
|
}
|
|
|
|
async function loadCapturedVideosFromDirectory(videoGallery, existingVideoCount) {
|
|
console.log(' Loading captured videos from directory...');
|
|
|
|
try {
|
|
// Get the selected recording directory
|
|
const savedDirectory = localStorage.getItem('webcamRecordingDirectory');
|
|
if (!savedDirectory) {
|
|
console.log(' No recording directory set, skipping captured videos');
|
|
return 0;
|
|
}
|
|
|
|
// Use Electron's IPC to read the video directory
|
|
if (window.electronAPI && window.electronAPI.readVideoDirectory) {
|
|
const videoFiles = await window.electronAPI.readVideoDirectory(savedDirectory);
|
|
const capturedVideoFiles = videoFiles.filter(file =>
|
|
file.name.startsWith('quick-play-session-') &&
|
|
(file.name.endsWith('.mp4') || file.name.endsWith('.webm'))
|
|
);
|
|
|
|
console.log(` Found ${capturedVideoFiles.length} captured session videos`);
|
|
|
|
capturedVideoFiles.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item captured-video-item';
|
|
|
|
// Extract timestamp from filename
|
|
const timestampMatch = video.name.match(/quick-play-session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);
|
|
const timestamp = timestampMatch ? timestampMatch[1].replace(/-/g, ':').replace('T', ' ') : 'Unknown';
|
|
|
|
videoElement.innerHTML = `
|
|
<div class="video-thumbnail-container">
|
|
<video class="video-thumbnail" preload="metadata" muted crossorigin="anonymous">
|
|
<source src="${video.url || video.path}#t=2" type="${video.type || 'video/mp4'}">
|
|
</video>
|
|
<div class="video-overlay">
|
|
<div class="play-icon"></div>
|
|
</div>
|
|
<div class="video-fallback" style="display: none;">
|
|
<div class="video-icon"></div>
|
|
</div>
|
|
</div>
|
|
<div class="video-info">
|
|
<div class="video-name">Session Recording ${index + 1}</div>
|
|
<div class="video-directory">Captured � ${timestamp}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Handle video thumbnail loading
|
|
const videoThumb = videoElement.querySelector('.video-thumbnail');
|
|
const fallback = videoElement.querySelector('.video-fallback');
|
|
|
|
videoThumb.addEventListener('loadedmetadata', function() {
|
|
console.log(`? Captured video thumbnail loaded: Recording ${index + 1}`);
|
|
this.currentTime = 2;
|
|
});
|
|
|
|
videoThumb.addEventListener('error', function(e) {
|
|
console.log(`? Captured video thumbnail failed: Recording ${index + 1}`, e);
|
|
videoThumb.style.display = 'none';
|
|
fallback.style.display = 'flex';
|
|
});
|
|
|
|
videoElement.addEventListener('click', function() {
|
|
previewVideo(video.url || video.path, `Session Recording ${index + 1}`);
|
|
});
|
|
|
|
videoGallery.appendChild(videoElement);
|
|
});
|
|
|
|
return capturedVideoFiles.length;
|
|
} else {
|
|
console.log(' Electron API not available, showing directory info instead');
|
|
|
|
// Fallback: Show directory info
|
|
const infoElement = document.createElement('div');
|
|
infoElement.className = 'gallery-item info-item';
|
|
infoElement.innerHTML = `
|
|
<div class="video-thumbnail-container">
|
|
<div class="video-fallback" style="display: flex; width: 100%; height: 150px; background: var(--bg-secondary); align-items: center; justify-content: center;">
|
|
<div class="video-icon" style="font-size: 2em;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="video-info">
|
|
<div class="video-name">Session Recordings</div>
|
|
<div class="video-directory">Saved to: ${savedDirectory}</div>
|
|
</div>
|
|
<div class="video-actions" style="text-align: center; padding: 10px;">
|
|
<small style="color: var(--text-dim);">Recorded videos are saved to your selected directory.</small>
|
|
</div>
|
|
`;
|
|
|
|
videoGallery.appendChild(infoElement);
|
|
return 1; // Return 1 for the info element
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading captured videos from directory:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async function populateVideoGallery(videos) {
|
|
const videoGallery = document.getElementById('lib-video-gallery');
|
|
if (!videoGallery) {
|
|
console.error('? lib-video-gallery element not found!');
|
|
return;
|
|
}
|
|
|
|
console.log(` Populating gallery with ${videos.length} videos`);
|
|
console.log(` Gallery element:`, videoGallery);
|
|
console.log(` Gallery innerHTML before clear:`, videoGallery.innerHTML.substring(0, 200));
|
|
|
|
if (videos.length === 0) {
|
|
videoGallery.innerHTML = `
|
|
<div class="no-videos-message">
|
|
<p>No videos found in linked directories</p>
|
|
<p>Click "Add Directory" to link a folder containing videos</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
videoGallery.innerHTML = '';
|
|
console.log(` Gallery cleared, innerHTML after clear:`, videoGallery.innerHTML);
|
|
console.log(` Gallery element classes:`, videoGallery.className);
|
|
console.log(` Gallery element style:`, videoGallery.style.cssText);
|
|
|
|
videos.forEach((video, index) => {
|
|
const videoElement = document.createElement('div');
|
|
videoElement.className = 'gallery-item video-item';
|
|
videoElement.innerHTML = `
|
|
<div class="video-thumbnail-container">
|
|
<video class="video-thumbnail" preload="metadata" muted crossorigin="anonymous">
|
|
<source src="${video.path}#t=2" type="video/mp4">
|
|
</video>
|
|
<div class="video-overlay">
|
|
<div class="play-icon"></div>
|
|
</div>
|
|
<div class="video-fallback" style="display: none;">
|
|
<div class="video-icon"></div>
|
|
</div>
|
|
</div>
|
|
<div class="video-info">
|
|
<div class="video-name">${video.name}</div>
|
|
<div class="video-directory">${video.directory}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Handle video thumbnail loading
|
|
const videoThumb = videoElement.querySelector('.video-thumbnail');
|
|
const fallback = videoElement.querySelector('.video-fallback');
|
|
|
|
videoThumb.addEventListener('loadedmetadata', function() {
|
|
console.log(`? Video thumbnail loaded for: ${video.name}`);
|
|
this.currentTime = 2; // Seek to 2 seconds
|
|
});
|
|
|
|
videoThumb.addEventListener('seeked', function() {
|
|
console.log(` Video thumbnail ready for: ${video.name}`);
|
|
});
|
|
|
|
videoThumb.addEventListener('error', function(e) {
|
|
console.log(`? Video thumbnail failed for: ${video.name}`, e);
|
|
// Show fallback icon if video fails to load
|
|
videoThumb.style.display = 'none';
|
|
fallback.style.display = 'flex';
|
|
});
|
|
|
|
videoElement.addEventListener('click', function() {
|
|
previewVideo(video.path, video.name);
|
|
});
|
|
|
|
videoGallery.appendChild(videoElement);
|
|
|
|
if (index < 5) {
|
|
console.log(` Added video ${index + 1}: ${video.name}`);
|
|
console.log(` Video element HTML:`, videoElement.outerHTML.substring(0, 200));
|
|
}
|
|
});
|
|
|
|
console.log(`? Created ${videos.length} linked video gallery items`);
|
|
|
|
// Add captured session videos from directory
|
|
const capturedVideosCount = await loadCapturedVideosFromDirectory(videoGallery, videos.length);
|
|
|
|
const totalVideos = videos.length + capturedVideosCount;
|
|
console.log(`? Total videos in gallery: ${totalVideos} (${videos.length} linked + ${capturedVideos.length} captured)`);
|
|
console.log(` Final gallery innerHTML length:`, videoGallery.innerHTML.length);
|
|
console.log(` Gallery children count:`, videoGallery.children.length);
|
|
console.log(` Gallery computed style display:`, window.getComputedStyle(videoGallery).display);
|
|
|
|
// Add a watcher to see if the gallery gets cleared
|
|
setTimeout(() => {
|
|
console.log(` AFTER 5 seconds - Gallery children count:`, videoGallery.children.length);
|
|
console.log(` AFTER 5 seconds - Gallery innerHTML length:`, videoGallery.innerHTML.length);
|
|
if (videoGallery.children.length === 0) {
|
|
console.log(`? GALLERY WAS CLEARED! Something removed all video elements.`);
|
|
} else {
|
|
console.log(`? Gallery still has content after 5 seconds`);
|
|
}
|
|
}, 5000);
|
|
|
|
// Debug: Check each video item's computed styles
|
|
Array.from(videoGallery.children).forEach((child, index) => {
|
|
if (index < 3) { // Check first 3 items
|
|
const styles = window.getComputedStyle(child);
|
|
console.log(` Video item ${index + 1} styles:`, {
|
|
display: styles.display,
|
|
width: styles.width,
|
|
height: styles.height,
|
|
visibility: styles.visibility,
|
|
opacity: styles.opacity,
|
|
position: styles.position,
|
|
top: styles.top,
|
|
left: styles.left
|
|
});
|
|
|
|
const container = child.querySelector('.video-thumbnail-container');
|
|
if (container) {
|
|
const containerStyles = window.getComputedStyle(container);
|
|
console.log(` Container ${index + 1} styles:`, {
|
|
display: containerStyles.display,
|
|
width: containerStyles.width,
|
|
height: containerStyles.height,
|
|
visibility: containerStyles.visibility,
|
|
opacity: containerStyles.opacity
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function previewVideo(videoUrl, videoName) {
|
|
let modal = document.getElementById('video-preview-modal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'video-preview-modal';
|
|
modal.className = 'video-preview-modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-backdrop" onclick="closeVideoPreview()"></div>
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<span id="preview-video-name">${videoName}</span>
|
|
<button class="close-btn" onclick="closeVideoPreview()"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<video id="preview-video" controls>
|
|
<source src="${videoUrl}" type="video/mp4">
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
} else {
|
|
document.getElementById('preview-video-name').textContent = videoName;
|
|
const videoElement = document.getElementById('preview-video');
|
|
videoElement.src = videoUrl;
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeVideoPreview() {
|
|
const modal = document.getElementById('video-preview-modal');
|
|
if (modal) {
|
|
const videoElement = document.getElementById('preview-video');
|
|
if (videoElement) {
|
|
videoElement.pause();
|
|
videoElement.currentTime = 0;
|
|
}
|
|
modal.style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
}
|
|
}
|
|
|
|
// Global functions for HTML onclick handlers
|
|
window.removeVideoDirectory = removeVideoDirectory;
|
|
window.closeVideoPreview = closeVideoPreview;
|
|
window.showVerificationPhotoPreview = showVerificationPhotoPreview;
|
|
window.downloadVerificationPhoto = downloadVerificationPhoto;
|
|
window.deleteVerificationPhoto = deleteVerificationPhoto;
|
|
|
|
// Test function for verification photos in main library
|
|
window.testVerificationPhotosInLibrary = function() {
|
|
console.log(' Testing verification photos integration...');
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
console.log(` Found ${verificationPhotos.length} verification photos in localStorage`);
|
|
|
|
if (verificationPhotos.length > 0) {
|
|
console.log(' First verification photo:', verificationPhotos[0]);
|
|
}
|
|
|
|
// Load the library to test integration
|
|
loadLinkedImages();
|
|
|
|
// Check if photos appear in "all" category
|
|
const allImages = window.allLinkedImages || [];
|
|
const verificationImagesInLibrary = allImages.filter(img => img.type === 'verification');
|
|
console.log(` Found ${verificationImagesInLibrary.length} verification photos in main library`);
|
|
|
|
if (verificationImagesInLibrary.length > 0) {
|
|
console.log('? Verification photos successfully integrated into main library!');
|
|
console.log(' Sample verification photo in library:', verificationImagesInLibrary[0]);
|
|
} else {
|
|
console.log(' Verification photos not found in main library');
|
|
}
|
|
|
|
// Test filter
|
|
const categoryFilter = document.getElementById('lib-image-category-filter');
|
|
if (categoryFilter) {
|
|
categoryFilter.value = 'verification';
|
|
categoryFilter.dispatchEvent(new Event('change'));
|
|
console.log(' Switched to verification filter');
|
|
}
|
|
|
|
// Test gallery tab
|
|
setupLibraryGalleryTab();
|
|
console.log('? Refreshed gallery tab to include verification photos');
|
|
|
|
return {
|
|
totalVerificationPhotos: verificationPhotos.length,
|
|
verificationPhotosInLibrary: verificationImagesInLibrary.length,
|
|
integrated: verificationImagesInLibrary.length > 0
|
|
};
|
|
};
|
|
|
|
// Test function specifically for the gallery tab
|
|
window.testGalleryIntegration = function() {
|
|
console.log(' Testing gallery tab verification photos...');
|
|
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
|
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
|
|
|
console.log(` Found ${capturedPhotos.length} captured photos`);
|
|
console.log(` Found ${verificationPhotos.length} verification photos`);
|
|
console.log(` Total should be: ${capturedPhotos.length + verificationPhotos.length} photos`);
|
|
|
|
// Debug verification photo structure
|
|
if (verificationPhotos.length > 0) {
|
|
console.log(' First verification photo structure:', verificationPhotos[0]);
|
|
console.log(' Properties:', Object.keys(verificationPhotos[0]));
|
|
console.log(' Has data property:', !!verificationPhotos[0].data);
|
|
console.log(' Has dataUrl property:', !!verificationPhotos[0].dataUrl);
|
|
console.log(' Image data type:', typeof (verificationPhotos[0].data || verificationPhotos[0].dataUrl));
|
|
}
|
|
|
|
// Refresh the gallery
|
|
setupLibraryGalleryTab();
|
|
|
|
// Check the count display
|
|
const countElement = document.getElementById('lib-all-photos-count');
|
|
if (countElement) {
|
|
console.log(' Gallery count display:', countElement.textContent);
|
|
}
|
|
|
|
return {
|
|
capturedPhotos: capturedPhotos.length,
|
|
verificationPhotos: verificationPhotos.length,
|
|
expectedTotal: capturedPhotos.length + verificationPhotos.length,
|
|
verificationStructure: verificationPhotos.length > 0 ? verificationPhotos[0] : null
|
|
};
|
|
};
|
|
|
|
// Test function to check XP system on main page
|
|
window.testMainPageXP = function() {
|
|
console.log(' Testing main page XP system...');
|
|
|
|
// Check PlayerStats availability
|
|
console.log(' PlayerStats available:', !!window.playerStats);
|
|
if (window.playerStats) {
|
|
const stats = window.playerStats.stats;
|
|
console.log(' Current total XP:', stats.totalXP);
|
|
console.log(' Current scenario XP:', stats.scenarioGameXP);
|
|
console.log(' Current quick play XP:', stats.quickPlayXP);
|
|
console.log(' Current porn cinema XP:', stats.pornCinemaXP);
|
|
}
|
|
|
|
// Check localStorage backup
|
|
const savedStats = localStorage.getItem('playerStats');
|
|
if (savedStats) {
|
|
try {
|
|
const stats = JSON.parse(savedStats);
|
|
console.log(' localStorage total XP:', stats.totalXP || 0);
|
|
console.log(' localStorage scenario XP:', stats.scenarioGameXP || 0);
|
|
} catch (e) {
|
|
console.log('? Error parsing localStorage stats');
|
|
}
|
|
}
|
|
|
|
// Force update the display
|
|
updateLevelDisplay();
|
|
|
|
// Test event system
|
|
if (window.playerStats) {
|
|
const beforeXP = window.playerStats.stats.totalXP;
|
|
window.playerStats.awardXP(1, 'test');
|
|
const afterXP = window.playerStats.stats.totalXP;
|
|
console.log(` Test XP award: ${beforeXP} ? ${afterXP} (+${afterXP - beforeXP})`);
|
|
|
|
// Force another display update
|
|
setTimeout(() => {
|
|
updateLevelDisplay();
|
|
console.log(' Display updated after test XP award');
|
|
}, 100);
|
|
|
|
return {
|
|
success: true,
|
|
beforeXP: beforeXP,
|
|
afterXP: afterXP,
|
|
awarded: afterXP - beforeXP
|
|
};
|
|
}
|
|
|
|
return { success: false, reason: 'PlayerStats not available' };
|
|
};
|
|
|
|
// ===== ANIMATION EVENT LISTENERS =====
|
|
|
|
// Listen for level up events
|
|
window.addEventListener('levelUp', (event) => {
|
|
console.log(' Level up event received:', event.detail);
|
|
updateLevelDisplay(); // Update the display immediately
|
|
});
|
|
|
|
// Listen for achievement events
|
|
window.addEventListener('achievementUnlocked', (event) => {
|
|
console.log(' Achievement unlocked event received:', event.detail);
|
|
});
|
|
|
|
// Listen for player stats updates
|
|
window.addEventListener('playerStatsUpdated', (event) => {
|
|
console.log(' Player stats updated:', event.detail);
|
|
updateLevelDisplay(); // Keep display in sync
|
|
});
|
|
|
|
// Test function for animations
|
|
window.testLevelUpAnimation = function() {
|
|
const oldLevel = { level: 4, name: 'Aroused', icon: '', description: 'Your arousal is building' };
|
|
const newLevel = { level: 5, name: 'Lustful', icon: '', description: 'Consumed by lust and craving more' };
|
|
window.showLevelUpAnimation(oldLevel, newLevel, 25, 'test');
|
|
};
|
|
|
|
window.testAchievementAnimation = function() {
|
|
const achievement = {
|
|
id: 'test',
|
|
icon: '',
|
|
title: 'Test Achievement',
|
|
description: 'This is a test achievement for demonstration'
|
|
};
|
|
window.showAchievementAnimation(achievement);
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
|
|
|