import {Theme} from '.'; import AddonHooks from '../../addons/hooks'; import {applyThemeFonts} from '../theme-fonts'; import './global-styles.css'; const BLOCK_COLOR_NAMES = [ // Corresponds to the name of the object in blockColors 'motion', 'looks', 'sounds', 'control', 'event', 'sensing', 'pen', 'operators', 'data', 'data_lists', 'more', 'addons' ]; /** * @param {string} css CSS color or var(--...) * @returns {string} evaluated CSS */ const evaluateCSS = css => { const variableMatch = css.match(/^var\(([\w-]+)\)$/); if (variableMatch) { return document.documentElement.style.getPropertyValue(variableMatch[1]); } return css; }; /** * Convert hex color to rgba with given opacity * @param {string} hex hex color string * @param {number} opacity opacity value (0-1) * @returns {string} rgba color string */ const hexToRgba = (hex, opacity) => { const cleanHex = hex.replace('#', ''); const r = parseInt(cleanHex.substr(0, 2), 16); const g = parseInt(cleanHex.substr(2, 2), 16); const b = parseInt(cleanHex.substr(4, 2), 16); return `rgba(${r}, ${g}, ${b}, ${opacity})`; }; /** * @param {Theme} theme the theme */ const applyGuiColors = theme => { const doc = document.documentElement; const defaultGuiColors = Theme.light.getGuiColors(); for (const [name, value] of Object.entries(defaultGuiColors)) { doc.style.setProperty(`--${name}-default`, value); } const guiColors = theme.getGuiColors(); for (const [name, value] of Object.entries(guiColors)) { doc.style.setProperty(`--${name}`, value); // Convert hex colors to RGB values for overlay purposes if (name === 'ui-primary' && value.startsWith('#')) { const hex = value.replace('#', ''); const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); doc.style.setProperty('--ui-primary-rgb', `${r}, ${g}, ${b}`); } else if (name === 'ui-primary' && value.startsWith('hsla')) { // For HSLA values, set a default RGB fallback doc.style.setProperty('--ui-primary-rgb', '229, 240, 255'); } } const blockColors = theme.getBlockColors(); doc.style.setProperty('--editorTheme3-blockText', blockColors.text); doc.style.setProperty('--editorTheme3-inputColor', blockColors.textField); doc.style.setProperty('--editorTheme3-inputColor-text', blockColors.textFieldText); for (const color of BLOCK_COLOR_NAMES) { doc.style.setProperty(`--editorTheme3-${color}-primary`, blockColors[color].primary); doc.style.setProperty(`--editorTheme3-${color}-secondary`, blockColors[color].secondary); doc.style.setProperty(`--editorTheme3-${color}-tertiary`, blockColors[color].tertiary); doc.style.setProperty(`--editorTheme3-${color}-field-background`, blockColors[color].quaternary); } // Some browsers will color their interfaces to match theme-color, so if we make it the same color as our // menu bar, it'll look pretty cool. let metaThemeColor = document.head.querySelector('meta[name=theme-color]'); if (!metaThemeColor) { metaThemeColor = document.createElement('meta'); metaThemeColor.setAttribute('name', 'theme-color'); document.head.appendChild(metaThemeColor); } metaThemeColor.setAttribute('content', evaluateCSS(guiColors['menu-bar-background'])); // a horrible hack for icons... window.Recolor = { primary: guiColors['looks-secondary'] }; AddonHooks.recolorCallbacks.forEach(i => i()); // Apply wallpaper applyWallpaper(theme.wallpaper); // Apply fonts (async but don't block UI) applyThemeFonts(theme.fonts).catch(console.error); }; /** * Apply wallpaper background to the GUI * @param {Object} wallpaper wallpaper configuration */ const applyWallpaper = wallpaper => { const target = document.querySelector("[class*='gui_body-wrapper_']"); let checkCountTarget = 0; if (!target) { const maxChecks = 50; const checkInterval = setInterval(() => { checkCountTarget++; const newTarget = document.querySelector("[class*='gui_body-wrapper_']"); if (newTarget) { applyWallpaper(wallpaper); clearInterval(checkInterval); } else if (checkCountTarget >= maxChecks) { clearInterval(checkInterval); } }, 500); return; } if (wallpaper.url) { // Apply opacity by creating a semi-transparent overlay const opacity = Math.max(0.1, Math.min(1, wallpaper.opacity || 0.3)); const overlayOpacity = 1 - opacity; // Apply darkness tinting with black overlay const darkness = Math.max(0, Math.min(0.8, wallpaper.darkness || 0)); // Create a composite background with the image and darkness overlay // The darkness overlay is applied as a black semi-transparent layer over the image if (darkness > 0) { target.style.backgroundImage = ` linear-gradient(rgba(0, 0, 0, ${darkness}), rgba(0, 0, 0, ${darkness})), url("${wallpaper.url}") `; } else { target.style.backgroundImage = `url("${wallpaper.url}")`; } target.style.backgroundSize = 'cover'; target.style.backgroundPosition = 'center'; target.style.backgroundRepeat = 'no-repeat'; target.style.backgroundAttachment = 'fixed'; // Use CSS custom properties for overlay and darkness document.documentElement.style.setProperty('--wallpaper-overlay-opacity', overlayOpacity.toString()); document.documentElement.style.setProperty('--wallpaper-darkness', darkness.toString()); // Apply JavaScript-based transparency to blocks workspace applyBlocksWorkspaceTransparency(true, opacity); // Update observer state for future blocks workspace changes updateWallpaperObserverState(true, opacity); // Also set up a periodic check for blocks workspace in case it loads later let checkCount = 0; const maxChecks = 50; const checkInterval = setInterval(() => { checkCount++; const blocksSvg = document.querySelector('svg.blocklySvg'); if (blocksSvg) { // Found the blocks workspace, apply transparency and stop checking applyTransparencyToElement(blocksSvg, true, opacity); clearInterval(checkInterval); } else if (checkCount >= maxChecks) { // Stop checking after max attempts clearInterval(checkInterval); } }, 500); } else { // Remove wallpaper target.style.backgroundImage = ''; target.style.backgroundSize = ''; target.style.backgroundPosition = ''; target.style.backgroundRepeat = ''; target.style.backgroundAttachment = ''; document.documentElement.style.removeProperty('--wallpaper-overlay-opacity'); document.documentElement.style.removeProperty('--wallpaper-darkness'); // Remove transparency from blocks workspace applyBlocksWorkspaceTransparency(false); // Update observer state updateWallpaperObserverState(false); } }; /** * Apply or remove transparency from the blocks workspace using JavaScript * @param {boolean} hasWallpaper whether wallpaper is active * @param {number} wallpaperOpacity opacity of the wallpaper (0.1 to 1.0) * @param {number} retryCount current retry attempt (for internal use) */ const applyBlocksWorkspaceTransparency = (hasWallpaper, wallpaperOpacity = 0.3, retryCount = 0) => { // Find the blocks workspace SVG element using the specific selector const blocksSvg = document.querySelector("svg.blocklySvg"); if (!blocksSvg) { // Fallback to a more general selector if the specific one doesn't work const fallbackSvg = document.querySelector('svg.blocklySvg'); if (fallbackSvg) { applyTransparencyToElement(fallbackSvg, hasWallpaper, wallpaperOpacity); return; } // If no blocks workspace is found and we haven't retried too many times, try again // This handles cases where wallpaper is applied before blocks are loaded const maxRetries = 20; // Try for up to 10 seconds (50ms * 20 = 1000ms, then exponential backoff) if (retryCount < maxRetries) { // Use exponential backoff: start with 50ms, then increase const delay = retryCount < 10 ? 50 : Math.min(500, 50 * Math.pow(2, retryCount - 10)); setTimeout(() => { applyBlocksWorkspaceTransparency(hasWallpaper, wallpaperOpacity, retryCount + 1); }, delay); } return; } applyTransparencyToElement(blocksSvg, hasWallpaper, wallpaperOpacity); }; /** * Apply transparency styling to a specific element * @param {Element} element the element to apply transparency to * @param {boolean} hasWallpaper whether wallpaper is active * @param {number} wallpaperOpacity opacity of the wallpaper (0.1 to 1.0) */ const applyTransparencyToElement = (element, hasWallpaper, wallpaperOpacity = 0.3) => { if (hasWallpaper) { // Calculate appropriate background transparency based on wallpaper opacity // Higher wallpaper opacity means we need more workspace transparency to see through const backgroundOpacity = Math.max(0.2, Math.min(0.8, 1 - wallpaperOpacity + 0.1)); const guiColors = document.documentElement.style.getPropertyValue('--ui-primary') || '#e5f0ff'; const backgroundColor = guiColors.startsWith('#') ? hexToRgba(guiColors, backgroundOpacity) : `rgba(229, 240, 255, ${backgroundOpacity})`; element.style.backgroundColor = backgroundColor; } else { // Remove transparency styling element.style.backgroundColor = ''; } }; // Keep track of the current wallpaper state for observer let currentWallpaperState = {hasWallpaper: false, opacity: 0.3}; /** * Observer to watch for blocks workspace changes and apply transparency when needed */ const createBlocksWorkspaceObserver = () => { // Only create observer if we don't already have one if (window.blocksWorkspaceObserver) { return; } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { // Check for both specific and general blocks workspace selectors const blocksSvg = document.querySelector('svg.blocklySvg'); if (blocksSvg && currentWallpaperState.hasWallpaper) { // Apply transparency to newly created blocks workspace applyTransparencyToElement(blocksSvg, true, currentWallpaperState.opacity); // Also check for any nested SVG elements that might need transparency const nestedSvgs = blocksSvg.querySelectorAll('svg'); nestedSvgs.forEach(svg => { applyTransparencyToElement(svg, true, currentWallpaperState.opacity); }); break; } } } }); // Observe the entire document for changes observer.observe(document.body, { childList: true, subtree: true }); window.blocksWorkspaceObserver = observer; }; /** * Update the observer's knowledge of current wallpaper state * @param {boolean} hasWallpaper whether wallpaper is active * @param {number} opacity wallpaper opacity */ const updateWallpaperObserverState = (hasWallpaper, opacity = 0.3) => { currentWallpaperState = {hasWallpaper, opacity}; if (hasWallpaper) { createBlocksWorkspaceObserver(); // Also add a listener for workspace creation events if available if (window.AddonHooks && window.AddonHooks.workspaceCreated) { window.AddonHooks.workspaceCreated(() => { if (currentWallpaperState.hasWallpaper) { // Small delay to ensure workspace is fully initialized setTimeout(() => { applyBlocksWorkspaceTransparency(true, currentWallpaperState.opacity); }, 50); } }); } } else if (window.blocksWorkspaceObserver) { // Clean up observer when no wallpaper is active window.blocksWorkspaceObserver.disconnect(); window.blocksWorkspaceObserver = null; } }; export { applyGuiColors, applyWallpaper, applyBlocksWorkspaceTransparency };