// ==UserScript== // @name [Global] Seasonal Decorations // @namespace https://github.com/kevingrillet // @author Kevin GRILLET // @description Adds event-based decorations (Christmas, Valentine's Day, Halloween, etc.) in background during event week. Configurable per site via CSV-like format. // @copyright https://github.com/kevingrillet // @license GPL-3.0 License // @tag kevingrillet // @tag all-sites // @tag decorations // @version 1.1.0 // @homepageURL https://github.com/kevingrillet/Userscripts/ // @supportURL https://github.com/kevingrillet/Userscripts/issues // @downloadURL https://raw.githubusercontent.com/kevingrillet/Userscripts/main/user.js/[Global]%20Seasonal%20Decorations.user.js // @updateURL https://raw.githubusercontent.com/kevingrillet/Userscripts/main/user.js/[Global]%20Seasonal%20Decorations.user.js // @match http*://*/* // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (function () { 'use strict'; const CONFIG_KEY = 'seasonal_decorations_config'; const DEBUG_MODE_KEY = 'seasonal_decorations_debug_mode'; const DEFAULT_CONFIG = `# Configuration format: baseurl; selector; color # Examples: www.google.fr; body; white github.com; .application-main; #ffffff # youtube.com; body; rgba(255,255,255,0.8)`; // Events definition with dates (month/day) const EVENTS = [ { name: 'christmas', month: 12, day: 25, window: 7 }, // Noël ± 7 jours { name: 'newyear', month: 1, day: 1, window: 3 }, // Nouvel an ± 3 jours { name: 'chandeleur', month: 2, day: 2, window: 3 }, // Chandeleur ± 3 jours { name: 'valentine', month: 2, day: 14, window: 3 }, // Saint-Valentin ± 3 jours { name: 'aprilfools', month: 4, day: 1, window: 1 }, // Poisson d'avril ± 1 jour { name: 'musicday', month: 6, day: 21, window: 3 }, // Fête de la musique ± 3 jours { name: 'halloween', month: 10, day: 31, window: 7 }, // Halloween ± 7 jours ]; // Calculate Easter date (Computus algorithm) function getEasterDate(year) { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return { month, day }; } // Check if date is within event window function isWithinEventWindow(eventMonth, eventDay, windowDays, currentMonth, currentDay, currentYear) { const eventDate = new Date(currentYear, eventMonth - 1, eventDay); const currentDate = new Date(currentYear, currentMonth - 1, currentDay); const diffTime = Math.abs(currentDate - eventDate); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays <= windowDays; } // Get current active event function getCurrentEvent() { // Check if debug mode is enabled const debugMode = GM_getValue(DEBUG_MODE_KEY, false); if (debugMode) { return 'christmas'; // Use Christmas decorations for debug } const now = new Date(); const month = now.getMonth() + 1; // 1-12 const day = now.getDate(); const year = now.getFullYear(); // Check fixed events for (const event of EVENTS) { if (isWithinEventWindow(event.month, event.day, event.window, month, day, year)) { return event.name; } } // Check Easter (variable date) const easter = getEasterDate(year); if (isWithinEventWindow(easter.month, easter.day, 3, month, day, year)) { return 'easter'; } return null; } // Parse CSV-like configuration function parseConfig(configText) { const lines = configText.split('\n'); const configs = []; for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith('#')) continue; const parts = trimmed.split(';').map(p => p.trim()); if (parts.length >= 2) { configs.push({ baseUrl: parts[0], selector: parts[1], color: parts[2] || 'white' }); } } return configs; } // Check if current URL matches a base URL pattern function matchesBaseUrl(currentHost, baseUrl) { return currentHost.includes(baseUrl) || baseUrl.includes(currentHost); } // Get matching configuration for current site function getMatchingConfig() { const configText = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG); const configs = parseConfig(configText); const currentHost = window.location.hostname; for (const config of configs) { if (matchesBaseUrl(currentHost, config.baseUrl)) { return config; } } return null; } // Create snowflake decoration function createSnowflake(color) { return ` `; } // Create easter egg decoration function createEasterEgg(color) { return ` `; } // Create pumpkin decoration (Halloween) function createPumpkin(color) { return ` `; } // Create heart decoration (Valentine's Day) function createHeart(color) { return ` `; } // Create music note decoration (Fête de la musique) function createMusicNote(color) { return ` `; } // Create pancake decoration (Chandeleur) function createPancake(color) { return ` `; } // Create fish decoration (April Fools) function createFish(color) { return ` `; } // Create firework decoration (New Year) function createFirework(color) { return ` `; } // Get decoration based on current event function getEventDecoration(color) { const event = getCurrentEvent(); switch (event) { case 'christmas': return createSnowflake(color); case 'newyear': return createFirework(color); case 'chandeleur': return createPancake(color); case 'valentine': return createHeart(color); case 'easter': return createEasterEgg(color); case 'aprilfools': return createFish(color); case 'musicday': return createMusicNote(color); case 'halloween': return createPumpkin(color); default: return null; } } // Add decorations to element function addDecorations(element, color) { const decoration = getEventDecoration(color); if (!decoration) return; // Create container for decorations const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999; overflow: hidden; `; container.className = 'seasonal-decorations-container'; // Create multiple decoration elements const count = 20; // Number of decorations for (let i = 0; i < count; i++) { const item = document.createElement('div'); item.style.cssText = ` position: absolute; pointer-events: none; opacity: 0; `; // Random starting position item.style.left = Math.random() * 100 + '%'; item.style.top = -50 + 'px'; // Set SVG content item.innerHTML = decoration; // Animation properties const duration = 10 + Math.random() * 20; // 10-30 seconds const delay = Math.random() * 5; // 0-5 seconds delay const endPosition = 100 + Math.random() * 20; // Fall beyond viewport // Apply animation item.style.animation = `fall${i} ${duration}s linear ${delay}s infinite`; // Create unique keyframes for each item const styleSheet = document.createElement('style'); styleSheet.textContent = ` @keyframes fall${i} { 0% { transform: translateY(0) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { transform: translateY(${endPosition}vh) rotate(${360 + Math.random() * 360}deg); opacity: 0; } } `; document.head.appendChild(styleSheet); container.appendChild(item); } document.body.appendChild(container); } // Edit configuration function editConfig() { const currentConfig = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG); const newConfig = prompt( 'Edit seasonal decorations configuration:\nFormat: baseurl; selector; color\n(One per line, # for comments)', currentConfig ); if (newConfig !== null) { GM_setValue(CONFIG_KEY, newConfig); alert('Configuration saved! Reload the page to apply changes.'); } } // Toggle debug mode function toggleDebugMode() { const currentMode = GM_getValue(DEBUG_MODE_KEY, false); const newMode = !currentMode; GM_setValue(DEBUG_MODE_KEY, newMode); if (newMode) { alert('Debug mode enabled! Snowflakes will appear on configured sites.\nReload the page to see decorations.'); } else { alert('Debug mode disabled! Decorations will only show during actual events.\nReload the page to apply changes.'); } } // Initialize function init() { // Register menu commands const debugMode = GM_getValue(DEBUG_MODE_KEY, false); const debugModeLabel = debugMode ? '✅ Debug Mode: ON' : '❌ Debug Mode: OFF'; GM_registerMenuCommand('⚙️ Edit Seasonal Decorations Config', editConfig); GM_registerMenuCommand(debugModeLabel + ' (Toggle)', toggleDebugMode); // Get configuration for current site const config = getMatchingConfig(); if (!config) { console.log('[Seasonal Decorations] No configuration for this site'); return; } // Wait for element to be available let attempts = 0; const maxAttempts = 20; // 10 seconds (20 * 500ms) const checkElement = setInterval(() => { attempts++; const element = document.querySelector(config.selector); if (element) { clearInterval(checkElement); addDecorations(element, config.color); const event = getCurrentEvent(); console.log(`[Seasonal Decorations] Applied ${event} decorations to ${config.selector} with color ${config.color}`); } else if (attempts >= maxAttempts) { clearInterval(checkElement); console.log(`[Seasonal Decorations] Element ${config.selector} not found after ${maxAttempts * 500}ms`); } }, 500); } // Run after page is fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(init, 100); // Small delay to load last }); } else { setTimeout(init, 100); } })();