Major additions: - Web-based setup wizard (setup.php, setup_wizard.php, setup-wizard.js) - Production Docker configuration (docker-compose.prod.yml, .env.production) - Database initialization SQL files (deploy/init_settings.sql) - Template builder system with drag-and-drop UI - Advanced features (OAuth, CDN, enhanced analytics, monetization) - Comprehensive documentation (deployment guides, quick start, feature docs) - Design system with accessibility and responsive layout - Deployment automation scripts (deploy.ps1, generate-secrets.ps1) Setup wizard allows customization of: - Platform name and branding - Domain configuration - Membership tiers and pricing - Admin credentials - Feature toggles Database includes 270+ tables for complete video streaming platform with advanced features for analytics, moderation, template building, and monetization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
515 lines
14 KiB
JavaScript
515 lines
14 KiB
JavaScript
/**
|
|
* EasyStream Advanced Theme Switcher
|
|
* Smooth theme transitions with system preference detection
|
|
* Version: 2.0
|
|
*/
|
|
|
|
class ThemeSwitcher {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
storageKey: 'easystream-theme',
|
|
colorStorageKey: 'easystream-color',
|
|
transitionDuration: 300,
|
|
detectSystemPreference: true,
|
|
...options
|
|
};
|
|
|
|
this.themes = {
|
|
light: ['blue', 'red', 'cyan', 'green', 'orange', 'pink', 'purple'],
|
|
dark: ['darkblue', 'darkred', 'darkcyan', 'darkgreen', 'darkorange', 'darkpink', 'darkpurple']
|
|
};
|
|
|
|
this.currentTheme = null;
|
|
this.currentColor = null;
|
|
this.systemPreference = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize theme switcher
|
|
*/
|
|
init() {
|
|
// Detect system preference
|
|
if (this.options.detectSystemPreference) {
|
|
this.detectSystemPreference();
|
|
this.watchSystemPreference();
|
|
}
|
|
|
|
// Load saved theme or use default
|
|
const savedTheme = this.getSavedTheme();
|
|
const savedColor = this.getSavedColor();
|
|
|
|
if (savedTheme && savedColor) {
|
|
this.applyTheme(savedTheme, savedColor, false);
|
|
} else if (this.systemPreference) {
|
|
// Use system preference
|
|
const defaultColor = 'blue';
|
|
const theme = this.systemPreference === 'dark' ? `dark${defaultColor}` : defaultColor;
|
|
this.applyTheme(this.systemPreference, defaultColor, false);
|
|
} else {
|
|
// Default to light blue
|
|
this.applyTheme('light', 'blue', false);
|
|
}
|
|
|
|
// Setup UI controls if they exist
|
|
this.setupControls();
|
|
}
|
|
|
|
/**
|
|
* Detect system color scheme preference
|
|
*/
|
|
detectSystemPreference() {
|
|
if (window.matchMedia) {
|
|
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
this.systemPreference = darkModeQuery.matches ? 'dark' : 'light';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watch for system preference changes
|
|
*/
|
|
watchSystemPreference() {
|
|
if (window.matchMedia) {
|
|
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
darkModeQuery.addEventListener('change', (e) => {
|
|
this.systemPreference = e.matches ? 'dark' : 'light';
|
|
|
|
// Only auto-switch if user hasn't manually set a theme
|
|
const savedTheme = this.getSavedTheme();
|
|
if (!savedTheme || savedTheme === 'auto') {
|
|
this.applySystemTheme();
|
|
}
|
|
|
|
this.dispatchEvent('system-preference-change', {
|
|
preference: this.systemPreference
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply system theme based on preference
|
|
*/
|
|
applySystemTheme() {
|
|
const color = this.currentColor || 'blue';
|
|
const theme = this.systemPreference === 'dark' ? `dark${color}` : color;
|
|
this.applyTheme(this.systemPreference, color, true);
|
|
}
|
|
|
|
/**
|
|
* Get saved theme from storage
|
|
*/
|
|
getSavedTheme() {
|
|
try {
|
|
return localStorage.getItem(this.options.storageKey);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get saved color from storage
|
|
*/
|
|
getSavedColor() {
|
|
try {
|
|
return localStorage.getItem(this.options.colorStorageKey);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save theme to storage
|
|
*/
|
|
saveTheme(mode, color) {
|
|
try {
|
|
localStorage.setItem(this.options.storageKey, mode);
|
|
localStorage.setItem(this.options.colorStorageKey, color);
|
|
} catch (e) {
|
|
console.warn('Failed to save theme preference', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply theme with smooth transition
|
|
* @param {string} mode - 'light' or 'dark'
|
|
* @param {string} color - color name (blue, red, etc.)
|
|
* @param {boolean} animate - whether to animate the transition
|
|
*/
|
|
applyTheme(mode, color, animate = true) {
|
|
const fullTheme = mode === 'dark' ? `dark${color}` : color;
|
|
|
|
// Prevent unnecessary updates
|
|
if (this.currentTheme === mode && this.currentColor === color) {
|
|
return;
|
|
}
|
|
|
|
const previousMode = this.currentTheme;
|
|
this.currentTheme = mode;
|
|
this.currentColor = color;
|
|
|
|
// Add transition class for smooth animation
|
|
if (animate) {
|
|
this.enableTransitions();
|
|
}
|
|
|
|
// Update body/html data attribute
|
|
document.documentElement.setAttribute('data-theme', fullTheme);
|
|
document.body.setAttribute('data-theme', fullTheme);
|
|
|
|
// Update meta theme-color for mobile browsers
|
|
this.updateMetaThemeColor();
|
|
|
|
// Save preference
|
|
this.saveTheme(mode, color);
|
|
|
|
// Remove transition class after animation
|
|
if (animate) {
|
|
setTimeout(() => {
|
|
this.disableTransitions();
|
|
}, this.options.transitionDuration);
|
|
}
|
|
|
|
// Update UI controls
|
|
this.updateControls();
|
|
|
|
// Dispatch custom event
|
|
this.dispatchEvent('theme-change', {
|
|
mode,
|
|
color,
|
|
fullTheme,
|
|
previousMode
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enable smooth transitions during theme switch
|
|
*/
|
|
enableTransitions() {
|
|
const style = document.createElement('style');
|
|
style.id = 'theme-transition-styles';
|
|
style.textContent = `
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
transition: background-color ${this.options.transitionDuration}ms ease,
|
|
color ${this.options.transitionDuration}ms ease,
|
|
border-color ${this.options.transitionDuration}ms ease,
|
|
box-shadow ${this.options.transitionDuration}ms ease !important;
|
|
}
|
|
|
|
/* Disable transitions for animations that shouldn't be affected */
|
|
.no-theme-transition,
|
|
[class*="animate"],
|
|
.video-js,
|
|
.spinner {
|
|
transition: none !important;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
/**
|
|
* Disable transitions after theme switch
|
|
*/
|
|
disableTransitions() {
|
|
const style = document.getElementById('theme-transition-styles');
|
|
if (style) {
|
|
style.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update meta theme-color for mobile browsers
|
|
*/
|
|
updateMetaThemeColor() {
|
|
let themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
|
|
|
if (!themeColorMeta) {
|
|
themeColorMeta = document.createElement('meta');
|
|
themeColorMeta.name = 'theme-color';
|
|
document.head.appendChild(themeColorMeta);
|
|
}
|
|
|
|
// Color mapping
|
|
const colors = {
|
|
blue: '#06a2cb',
|
|
red: '#dd1e2f',
|
|
cyan: '#00997a',
|
|
green: '#199900',
|
|
orange: '#f28410',
|
|
pink: '#ec7ab9',
|
|
purple: '#b25c8b'
|
|
};
|
|
|
|
const bgColors = {
|
|
light: '#ffffff',
|
|
dark: '#121212'
|
|
};
|
|
|
|
// Use primary color for theme
|
|
const primaryColor = colors[this.currentColor] || colors.blue;
|
|
const bgColor = bgColors[this.currentTheme] || bgColors.light;
|
|
|
|
themeColorMeta.content = this.currentTheme === 'dark' ? bgColor : primaryColor;
|
|
}
|
|
|
|
/**
|
|
* Toggle between light and dark mode
|
|
*/
|
|
toggleMode() {
|
|
const newMode = this.currentTheme === 'light' ? 'dark' : 'light';
|
|
this.applyTheme(newMode, this.currentColor, true);
|
|
}
|
|
|
|
/**
|
|
* Set color theme
|
|
* @param {string} color - color name
|
|
*/
|
|
setColor(color) {
|
|
if (!this.themes.light.includes(color) && !this.themes.dark.includes(color.replace('dark', ''))) {
|
|
console.warn(`Invalid color: ${color}`);
|
|
return;
|
|
}
|
|
|
|
const baseColor = color.replace('dark', '');
|
|
this.applyTheme(this.currentTheme, baseColor, true);
|
|
}
|
|
|
|
/**
|
|
* Setup UI controls
|
|
*/
|
|
setupControls() {
|
|
// Theme toggle button
|
|
const toggleBtn = document.getElementById('theme-toggle');
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', () => this.toggleMode());
|
|
}
|
|
|
|
// Color picker buttons
|
|
const colorBtns = document.querySelectorAll('[data-color-theme]');
|
|
colorBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const color = btn.getAttribute('data-color-theme');
|
|
this.setColor(color);
|
|
});
|
|
});
|
|
|
|
// Auto theme switch (follow system)
|
|
const autoSwitch = document.getElementById('theme-auto');
|
|
if (autoSwitch) {
|
|
autoSwitch.addEventListener('change', (e) => {
|
|
if (e.target.checked) {
|
|
this.applySystemTheme();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update UI controls to reflect current theme
|
|
*/
|
|
updateControls() {
|
|
// Update toggle button
|
|
const toggleBtn = document.getElementById('theme-toggle');
|
|
if (toggleBtn) {
|
|
const icon = toggleBtn.querySelector('i, .icon');
|
|
if (icon) {
|
|
if (this.currentTheme === 'dark') {
|
|
icon.className = 'icon-sun';
|
|
toggleBtn.setAttribute('aria-label', 'Switch to light mode');
|
|
} else {
|
|
icon.className = 'icon-moon';
|
|
toggleBtn.setAttribute('aria-label', 'Switch to dark mode');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update color buttons
|
|
const colorBtns = document.querySelectorAll('[data-color-theme]');
|
|
colorBtns.forEach(btn => {
|
|
const color = btn.getAttribute('data-color-theme');
|
|
if (color === this.currentColor) {
|
|
btn.classList.add('active');
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
} else {
|
|
btn.classList.remove('active');
|
|
btn.setAttribute('aria-pressed', 'false');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get current theme info
|
|
*/
|
|
getCurrentTheme() {
|
|
return {
|
|
mode: this.currentTheme,
|
|
color: this.currentColor,
|
|
fullTheme: this.currentTheme === 'dark' ? `dark${this.currentColor}` : this.currentColor,
|
|
systemPreference: this.systemPreference
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dispatch custom event
|
|
*/
|
|
dispatchEvent(eventName, detail) {
|
|
const event = new CustomEvent(`easystream:${eventName}`, {
|
|
detail,
|
|
bubbles: true
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Create theme picker UI
|
|
* @returns {HTMLElement}
|
|
*/
|
|
static createThemePicker() {
|
|
const container = document.createElement('div');
|
|
container.className = 'theme-picker';
|
|
container.setAttribute('role', 'toolbar');
|
|
container.setAttribute('aria-label', 'Theme controls');
|
|
|
|
container.innerHTML = `
|
|
<div class="theme-picker-header">
|
|
<h3>Appearance</h3>
|
|
</div>
|
|
|
|
<div class="theme-mode-toggle">
|
|
<label class="theme-label">
|
|
<span>Mode</span>
|
|
<button id="theme-toggle" class="btn btn-secondary touch-target" aria-label="Toggle theme mode">
|
|
<i class="icon-moon"></i>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="theme-color-picker">
|
|
<span class="theme-label">Color</span>
|
|
<div class="color-options" role="group" aria-label="Color themes">
|
|
<button class="color-btn color-blue" data-color-theme="blue" aria-label="Blue theme">
|
|
<span class="sr-only">Blue</span>
|
|
</button>
|
|
<button class="color-btn color-red" data-color-theme="red" aria-label="Red theme">
|
|
<span class="sr-only">Red</span>
|
|
</button>
|
|
<button class="color-btn color-cyan" data-color-theme="cyan" aria-label="Cyan theme">
|
|
<span class="sr-only">Cyan</span>
|
|
</button>
|
|
<button class="color-btn color-green" data-color-theme="green" aria-label="Green theme">
|
|
<span class="sr-only">Green</span>
|
|
</button>
|
|
<button class="color-btn color-orange" data-color-theme="orange" aria-label="Orange theme">
|
|
<span class="sr-only">Orange</span>
|
|
</button>
|
|
<button class="color-btn color-pink" data-color-theme="pink" aria-label="Pink theme">
|
|
<span class="sr-only">Pink</span>
|
|
</button>
|
|
<button class="color-btn color-purple" data-color-theme="purple" aria-label="Purple theme">
|
|
<span class="sr-only">Purple</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.theme-picker {
|
|
padding: var(--space-lg, 24px);
|
|
background: var(--color-bg-elevated);
|
|
border-radius: var(--border-radius-lg, 12px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.theme-picker-header h3 {
|
|
margin: 0 0 var(--space-md, 16px) 0;
|
|
font-size: var(--font-size-lg, 1.125rem);
|
|
font-weight: var(--font-weight-semibold, 600);
|
|
}
|
|
|
|
.theme-mode-toggle {
|
|
margin-bottom: var(--space-lg, 24px);
|
|
}
|
|
|
|
.theme-label {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-weight: var(--font-weight-medium, 500);
|
|
margin-bottom: var(--space-sm, 8px);
|
|
}
|
|
|
|
.color-options {
|
|
display: flex;
|
|
gap: var(--space-sm, 8px);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.color-btn {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: var(--border-radius-full, 9999px);
|
|
border: 3px solid transparent;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base, 200ms);
|
|
position: relative;
|
|
}
|
|
|
|
.color-btn:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.color-btn:focus-visible {
|
|
outline: 3px solid var(--focus-ring-color);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.color-btn.active {
|
|
border-color: var(--color-text-primary);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.color-btn.active::after {
|
|
content: '✓';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 1.25rem;
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.color-blue { background: #06a2cb; }
|
|
.color-red { background: #dd1e2f; }
|
|
.color-cyan { background: #00997a; }
|
|
.color-green { background: #199900; }
|
|
.color-orange { background: #f28410; }
|
|
.color-pink { background: #ec7ab9; }
|
|
.color-purple { background: #b25c8b; }
|
|
</style>
|
|
`;
|
|
|
|
return container;
|
|
}
|
|
}
|
|
|
|
// Auto-initialize on page load
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
window.themeSwitcher = new ThemeSwitcher();
|
|
|
|
// Make it globally accessible
|
|
window.EasyStream = window.EasyStream || {};
|
|
window.EasyStream.ThemeSwitcher = ThemeSwitcher;
|
|
});
|
|
}
|
|
|
|
// Export for module systems
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = ThemeSwitcher;
|
|
}
|