Add seasonal themes for Birthday, Olympia, Space, Sports, and Underwater: implement CSS and JavaScript for visual effects and configurations
This commit is contained in:
180
Jellyfin.Plugin.Seasonals/Web/underwater.js
Normal file
180
Jellyfin.Plugin.Seasonals/Web/underwater.js
Normal file
@@ -0,0 +1,180 @@
|
||||
const config = window.SeasonalsPluginConfig?.Underwater || {};
|
||||
|
||||
const underwater = config.EnableUnderwater !== undefined ? config.EnableUnderwater : true;
|
||||
const symbolCount = config.SymbolCount || 15;
|
||||
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleUnderwater() {
|
||||
const container = document.querySelector('.underwater-container');
|
||||
if (!container) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
container.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('Underwater hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Underwater visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleUnderwater);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createUnderwater() {
|
||||
const container = document.querySelector('.underwater-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.underwater-container')) {
|
||||
container.className = 'underwater-container';
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Deep blue overlay
|
||||
const bg = document.createElement('div');
|
||||
bg.className = 'underwater-bg';
|
||||
container.appendChild(bg);
|
||||
|
||||
const standardCount = 8;
|
||||
const totalSymbols = symbolCount + standardCount;
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let finalCount = totalSymbols;
|
||||
|
||||
if (isMobile) {
|
||||
finalCount = enableRandomMobile ? totalSymbols : standardCount;
|
||||
}
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
// Seaweed swaying at the bottom
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let seaweed = document.createElement('div');
|
||||
seaweed.className = 'underwater-seaweed';
|
||||
seaweed.style.left = `${10 + (i * 25)}vw`;
|
||||
seaweed.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
|
||||
// Randomly flip
|
||||
if (Math.random() > 0.5) {
|
||||
seaweed.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = '../Seasonals/Resources/underwater_images/seaweed.png';
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
this.parentElement.innerHTML = '🌿';
|
||||
this.parentElement.style.fontSize = '3rem';
|
||||
this.parentElement.style.bottom = '0';
|
||||
this.parentElement.style.transformOrigin = 'bottom center';
|
||||
};
|
||||
seaweed.appendChild(img);
|
||||
container.appendChild(seaweed);
|
||||
}
|
||||
|
||||
const activeItems = ['fish_orange', 'fish_blue', 'jellyfish', 'turtle'];
|
||||
|
||||
for (let i = 0; i < finalCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
|
||||
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||
symbol.className = `underwater-symbol underwater-${randomItem}`;
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = `../Seasonals/Resources/underwater_images/${randomItem}.png`;
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
this.parentElement.innerHTML = getUnderwaterEmojiFallback(randomItem);
|
||||
};
|
||||
symbol.appendChild(img);
|
||||
|
||||
const topPos = 10 + Math.random() * 80; // 10 to 90vh
|
||||
const delaySeconds = Math.random() * 10;
|
||||
|
||||
let durationSeconds = 15;
|
||||
if (useRandomDuration) {
|
||||
durationSeconds = Math.random() * 10 + 15; // 15 to 25 seconds slow swimming
|
||||
}
|
||||
|
||||
// Randomly pick direction: left-to-right OR right-to-left
|
||||
const goRight = Math.random() > 0.5;
|
||||
if (goRight) {
|
||||
symbol.style.animationName = 'underwater-swim-right';
|
||||
symbol.style.left = '-10vw';
|
||||
} else {
|
||||
symbol.style.animationName = 'underwater-swim-left';
|
||||
symbol.style.right = '-10vw';
|
||||
symbol.style.transform = 'scaleX(-1)'; // flip fish to face left
|
||||
}
|
||||
|
||||
symbol.style.top = `${topPos}vh`;
|
||||
symbol.style.animationDuration = `${durationSeconds}s`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
// Small vertical sway
|
||||
const swimSway = document.createElement('div');
|
||||
swimSway.style.animation = `underwater-sway ${Math.random() * 2 + 3}s ease-in-out infinite`;
|
||||
swimSway.appendChild(symbol.cloneNode(true));
|
||||
symbol.innerHTML = '';
|
||||
symbol.appendChild(swimSway);
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
|
||||
// Bubbles
|
||||
const bubbleCount = isMobile ? 15 : 30;
|
||||
|
||||
for (let i = 0; i < bubbleCount; i++) {
|
||||
let bubble = document.createElement('div');
|
||||
bubble.className = 'underwater-bubble';
|
||||
|
||||
const leftPos = Math.random() * 100;
|
||||
const delaySeconds = Math.random() * 8;
|
||||
const duration = Math.random() * 4 + 4; // 4 to 8s rising
|
||||
|
||||
bubble.style.left = `${leftPos}vw`;
|
||||
bubble.style.animationDuration = `${duration}s`;
|
||||
bubble.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
// randomize bubble size
|
||||
const size = Math.random() * 15 + 5;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
}
|
||||
|
||||
function getUnderwaterEmojiFallback(type) {
|
||||
if (type === 'fish_orange') return '🐠';
|
||||
if (type === 'fish_blue') return '🐟';
|
||||
if (type === 'jellyfish') return '🪼';
|
||||
if (type === 'turtle') return '🐢';
|
||||
return '🫧';
|
||||
}
|
||||
|
||||
function initializeUnderwater() {
|
||||
if (!underwater) return;
|
||||
createUnderwater();
|
||||
toggleUnderwater();
|
||||
}
|
||||
|
||||
initializeUnderwater();
|
||||
Reference in New Issue
Block a user