feat: Add complete Docker deployment with web-based setup wizard
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>
This commit is contained in:
514
f_scripts/shared/theme-switcher.js
Normal file
514
f_scripts/shared/theme-switcher.js
Normal file
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user