// ==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);
}
})();