Files
donate/static/js/theme.js
2025-09-27 17:07:58 +01:00

266 lines
8.8 KiB
JavaScript

// Theme Management with Draggable Fun Button
class ThemeManager {
constructor() {
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.position = { x: 0, y: 0 };
this.clickStartTime = 0;
this.clickThreshold = 200; // ms
this.init();
}
init() {
// Load saved theme or detect OS preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set initial theme
if (savedTheme) {
this.setTheme(savedTheme);
} else if (prefersDark) {
this.setTheme('dark');
} else {
this.setTheme('light');
}
// Listen for OS theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
// Set up toggle button with drag functionality
this.setupToggleButton();
this.loadPosition();
}
setTheme(theme) {
// Add smooth transition class temporarily
document.body.classList.add('theme-transitioning');
document.documentElement.setAttribute('data-theme', theme);
this.updateToggleButton(theme);
this.addPulseEffect();
// Remove transition class after animation completes
setTimeout(() => {
document.body.classList.remove('theme-transitioning');
}, 400);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
localStorage.setItem('theme', newTheme);
this.addBounceEffect();
}
setupToggleButton() {
const toggleButton = document.getElementById('theme-toggle');
if (!toggleButton) return;
// Simple click handler as fallback
toggleButton.addEventListener('click', (e) => {
// Only handle click if we haven't detected dragging
if (!this.isDragging) {
e.preventDefault();
e.stopPropagation();
this.toggleTheme();
}
});
// Mouse events for drag functionality
toggleButton.addEventListener('mousedown', this.handleStart.bind(this));
document.addEventListener('mousemove', this.handleMove.bind(this));
document.addEventListener('mouseup', this.handleEnd.bind(this));
// Touch events for mobile
toggleButton.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
document.addEventListener('touchend', this.handleEnd.bind(this));
// Prevent context menu on long press
toggleButton.addEventListener('contextmenu', (e) => e.preventDefault());
// Double-click for fun bounce effect
toggleButton.addEventListener('dblclick', (e) => {
e.preventDefault();
this.addBounceEffect();
this.snapToNearestCorner();
});
}
handleStart(e) {
e.preventDefault();
this.clickStartTime = Date.now();
this.isDragging = false;
this.startPosition = { x: 0, y: 0 };
const toggleButton = document.getElementById('theme-toggle');
const rect = toggleButton.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
this.dragOffset.x = clientX - rect.left;
this.dragOffset.y = clientY - rect.top;
this.startPosition.x = clientX;
this.startPosition.y = clientY;
toggleButton.classList.add('dragging');
}
handleMove(e) {
const toggleButton = document.getElementById('theme-toggle');
if (!toggleButton.classList.contains('dragging')) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
// Calculate distance moved from start position
const deltaX = Math.abs(clientX - this.startPosition.x);
const deltaY = Math.abs(clientY - this.startPosition.y);
const dragThreshold = 5; // pixels
// Only start dragging if moved more than threshold
if (deltaX > dragThreshold || deltaY > dragThreshold) {
this.isDragging = true;
e.preventDefault();
this.position.x = clientX - this.dragOffset.x;
this.position.y = clientY - this.dragOffset.y;
// Keep button within viewport bounds
const buttonSize = 60;
const margin = 10;
this.position.x = Math.max(margin, Math.min(window.innerWidth - buttonSize - margin, this.position.x));
this.position.y = Math.max(margin, Math.min(window.innerHeight - buttonSize - margin, this.position.y));
toggleButton.style.left = this.position.x + 'px';
toggleButton.style.top = this.position.y + 'px';
toggleButton.style.right = 'auto';
}
}
handleEnd(e) {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('dragging');
if (this.isDragging) {
// It was a drag, save position and add bounce effect
this.savePosition();
this.addBounceEffect();
// Prevent click event from firing after drag
setTimeout(() => {
this.isDragging = false;
}, 50);
} else {
this.isDragging = false;
}
}
savePosition() {
localStorage.setItem('themeButtonPosition', JSON.stringify(this.position));
// Update CSS variables for immediate positioning on reload
document.documentElement.style.setProperty('--theme-button-x', this.position.x + 'px');
document.documentElement.style.setProperty('--theme-button-y', this.position.y + 'px');
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
toggleButton.classList.add('positioned');
}
}
loadPosition() {
const savedPosition = localStorage.getItem('themeButtonPosition');
if (savedPosition) {
this.position = JSON.parse(savedPosition);
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
// Position is already set by CSS variables, just ensure the class is added
toggleButton.classList.add('positioned');
toggleButton.style.left = this.position.x + 'px';
toggleButton.style.top = this.position.y + 'px';
toggleButton.style.right = 'auto';
}
}
}
snapToNearestCorner() {
const toggleButton = document.getElementById('theme-toggle');
const buttonSize = 60;
const margin = 20;
const corners = [
{ x: margin, y: margin }, // top-left
{ x: window.innerWidth - buttonSize - margin, y: margin }, // top-right
{ x: margin, y: window.innerHeight - buttonSize - margin }, // bottom-left
{ x: window.innerWidth - buttonSize - margin, y: window.innerHeight - buttonSize - margin } // bottom-right
];
// Find nearest corner
let nearestCorner = corners[0];
let minDistance = Infinity;
corners.forEach(corner => {
const distance = Math.sqrt(
Math.pow(corner.x - this.position.x, 2) +
Math.pow(corner.y - this.position.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearestCorner = corner;
}
});
// Animate to nearest corner
this.position = nearestCorner;
toggleButton.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
toggleButton.style.left = nearestCorner.x + 'px';
toggleButton.style.top = nearestCorner.y + 'px';
setTimeout(() => {
toggleButton.style.transition = '';
this.savePosition();
}, 500);
}
addBounceEffect() {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('bounce');
// Trigger reflow
toggleButton.offsetHeight;
toggleButton.classList.add('bounce');
setTimeout(() => toggleButton.classList.remove('bounce'), 600);
}
addPulseEffect() {
const toggleButton = document.getElementById('theme-toggle');
toggleButton.classList.remove('pulse');
// Trigger reflow
toggleButton.offsetHeight;
toggleButton.classList.add('pulse');
setTimeout(() => toggleButton.classList.remove('pulse'), 1000);
}
updateToggleButton(theme) {
const toggleButton = document.getElementById('theme-toggle');
if (toggleButton) {
toggleButton.innerHTML = theme === 'dark' ? '☀️' : '🌙';
toggleButton.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`);
toggleButton.setAttribute('title', `Click to switch to ${theme === 'dark' ? 'light' : 'dark'} mode • Drag to move • Double-click to snap to corner`);
}
}
getCurrentTheme() {
return document.documentElement.getAttribute('data-theme');
}
}
// Initialize theme manager when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.themeManager = new ThemeManager();
});