import * as vars from './variables.js'; import { showNotification, hexToRgba, setupInputListener, setupOpacitySliderListener, _restoreOpacitySetting, _restoreColorPickerSetting, _restoreInputSetting, _restoreCheckboxSetting } from './utils.js'; let currentItems = []; // Holds the original list of items provided by the user let displayedItems = []; // Holds the items currently displayed on the board // --- Function to set the HTML background (solid or gradient) --- function setBackground() { const backgroundType = document.querySelector('input[name="background-type"]:checked').value; const solidColorPicker = document.getElementById('background-color-picker'); const gradientColor1Picker = document.getElementById('gradient-color-1'); const gradientColor2Picker = document.getElementById('gradient-color-2'); const gradientDirectionSelect = document.getElementById('gradient-direction'); const htmlStyle = document.documentElement.style; let backgroundValue = ''; if (backgroundType === 'solid') { const color = solidColorPicker.value; const opacity = parseInt(document.getElementById('background-color-opacity-slider').value, 10); backgroundValue = hexToRgba(color, opacity); htmlStyle.background = backgroundValue; // Set solid color as background htmlStyle.backgroundRepeat = 'no-repeat'; htmlStyle.backgroundAttachment = 'fixed'; } else { // gradient const color1 = gradientColor1Picker.value; const opacity1 = parseInt(document.getElementById('gradient-color-1-opacity-slider').value, 10); const color2 = gradientColor2Picker.value; const opacity2 = parseInt(document.getElementById('gradient-color-2-opacity-slider').value, 10); const direction = gradientDirectionSelect.value; const rgba1 = hexToRgba(color1, opacity1); const rgba2 = hexToRgba(color2, opacity2); let gradientValue; if (direction === 'radial') { gradientValue = `radial-gradient(circle, ${rgba1}, ${rgba2})`; } else { // Handles 'deg' and direction keywords gradientValue = `linear-gradient(${direction}, ${rgba1}, ${rgba2})`; } backgroundValue = gradientValue; htmlStyle.background = gradientValue; htmlStyle.backgroundRepeat = 'no-repeat'; htmlStyle.backgroundAttachment = 'fixed'; } // htmlStyle.background = backgroundValue; // Already set inside if/else return backgroundValue; // Return the CSS value for injection (might need adjustment?) } // --- Function to manage visibility of background controls --- function toggleBackgroundControls() { const backgroundType = document.querySelector('input[name="background-type"]:checked').value; const solidSettings = document.getElementById('solid-color-settings'); const gradientSettings = document.getElementById('gradient-color-settings'); if (backgroundType === 'solid') { solidSettings.style.display = 'block'; gradientSettings.style.display = 'none'; } else { solidSettings.style.display = 'none'; gradientSettings.style.display = 'block'; } } // --- Save current background settings to localStorage --- function saveBackgroundSettings() { const backgroundType = document.querySelector('input[name="background-type"]:checked').value; localStorage.setItem(vars.LS_BACKGROUND_TYPE, backgroundType); if (backgroundType === 'solid') { localStorage.setItem(vars.LS_SOLID_COLOR, document.getElementById('background-color-picker').value); localStorage.setItem(vars.LS_SOLID_COLOR_OPACITY, document.getElementById('background-color-opacity-slider').value); // Optionally remove gradient keys localStorage.removeItem(vars.LS_GRADIENT_COLOR_1); localStorage.removeItem(vars.LS_GRADIENT_COLOR_1_OPACITY); localStorage.removeItem(vars.LS_GRADIENT_COLOR_2); localStorage.removeItem(vars.LS_GRADIENT_COLOR_2_OPACITY); localStorage.removeItem(vars.LS_GRADIENT_DIRECTION); } else { localStorage.setItem(vars.LS_GRADIENT_COLOR_1, document.getElementById('gradient-color-1').value); localStorage.setItem(vars.LS_GRADIENT_COLOR_1_OPACITY, document.getElementById('gradient-color-1-opacity-slider').value); localStorage.setItem(vars.LS_GRADIENT_COLOR_2, document.getElementById('gradient-color-2').value); localStorage.setItem(vars.LS_GRADIENT_COLOR_2_OPACITY, document.getElementById('gradient-color-2-opacity-slider').value); localStorage.setItem(vars.LS_GRADIENT_DIRECTION, document.getElementById('gradient-direction').value); // Optionally remove solid key localStorage.removeItem(vars.LS_SOLID_COLOR); localStorage.removeItem(vars.LS_SOLID_COLOR_OPACITY); } } export function toggleConfig() { const pane = document.getElementById('config-pane'); const isOpen = pane.classList.contains('config-pane-open'); if (isOpen) { pane.classList.remove('config-pane-open'); pane.classList.add('config-pane-closed'); localStorage.setItem(vars.LS_CONFIG_OPEN, 'false'); } else { pane.classList.remove('config-pane-closed'); pane.classList.add('config-pane-open'); localStorage.setItem(vars.LS_CONFIG_OPEN, 'true'); } } document.getElementById('file-input').addEventListener('change', function (event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function (e) { document.getElementById('cell-contents').value = e.target.result; showNotification('File loaded. Click Generate/Update to use items.', 'success'); }; reader.onerror = function () { showNotification('Error reading file.', 'error'); }; reader.readAsText(file); } }); function getItemsFromInput() { const text = document.getElementById('cell-contents').value.trim(); return text ? text.split('\n').map(item => item.trim()).filter(item => item) : []; } // --- Save current marked style settings to localStorage --- function saveMarkedStyleSettings() { localStorage.setItem(vars.LS_MARKED_COLOR, document.getElementById('marked-color-picker').value); localStorage.setItem(vars.LS_MARKED_COLOR_OPACITY, document.getElementById('marked-color-opacity-slider').value); localStorage.setItem(vars.LS_MARKED_IMAGE_URL, document.getElementById('marked-image-url-input').value); localStorage.setItem(vars.LS_MARKED_IMAGE_OPACITY, document.getElementById('marked-image-opacity-slider').value); // Save image opacity // Save marked border color and its opacity localStorage.setItem(vars.LS_MARKED_BORDER_COLOR, document.getElementById('marked-border-color-picker').value); localStorage.setItem(vars.LS_MARKED_BORDER_OPACITY, document.getElementById('marked-border-opacity-slider').value); // Save marked border width localStorage.setItem(vars.LS_MARKED_BORDER_WIDTH, document.getElementById('marked-border-width-input').value); // Save marked text styles localStorage.setItem(vars.LS_MARKED_CELL_TEXT_COLOR, document.getElementById('marked-cell-text-color-picker').value); localStorage.setItem(vars.LS_MARKED_CELL_TEXT_OPACITY, document.getElementById('marked-cell-text-opacity-slider').value); localStorage.setItem(vars.LS_MARKED_CELL_OUTLINE_COLOR, document.getElementById('marked-cell-outline-color-picker').value); localStorage.setItem(vars.LS_MARKED_CELL_OUTLINE_OPACITY, document.getElementById('marked-cell-outline-opacity-slider').value); localStorage.setItem(vars.LS_MARKED_CELL_OUTLINE_WIDTH, document.getElementById('marked-cell-outline-width-input').value); } // --- Apply saved marked styles to a cell --- function applyMarkedCellStyle(cell) { if (!cell) return; const isMarked = cell.classList.contains('marked'); const color = localStorage.getItem(vars.LS_MARKED_COLOR) || vars.DEFAULT_MARKED_COLOR; const colorOpacity = parseInt(localStorage.getItem(vars.LS_MARKED_COLOR_OPACITY) || vars.DEFAULT_MARKED_COLOR_OPACITY, 10); const imageUrl = localStorage.getItem(vars.LS_MARKED_IMAGE_URL) || vars.DEFAULT_MARKED_IMAGE_URL; const imageOpacity = parseInt(localStorage.getItem(vars.LS_MARKED_IMAGE_OPACITY) || vars.DEFAULT_MARKED_IMAGE_OPACITY, 10); const borderColor = localStorage.getItem(vars.LS_MARKED_BORDER_COLOR) || vars.DEFAULT_MARKED_BORDER_COLOR; const borderOpacity = parseInt(localStorage.getItem(vars.LS_MARKED_BORDER_OPACITY) || vars.DEFAULT_MARKED_BORDER_OPACITY, 10); // Reset potentially conflicting styles before applying new ones cell.style.backgroundColor = ''; cell.style.backgroundImage = ''; // Don't reset border color here, let applyCellStyle handle default if unmarked if (isMarked) { // Apply marked border color + opacity cell.style.borderColor = hexToRgba(borderColor, borderOpacity); // Apply marked border width let markedBorderWidth = parseInt(localStorage.getItem(vars.LS_MARKED_BORDER_WIDTH) || vars.DEFAULT_MARKED_BORDER_WIDTH, 10); if (isNaN(markedBorderWidth) || markedBorderWidth < 0) { // Add check for NaN and negative markedBorderWidth = vars.DEFAULT_MARKED_BORDER_WIDTH; } cell.style.borderWidth = `${markedBorderWidth}px`; // Always apply the background color + opacity cell.style.backgroundColor = hexToRgba(color, colorOpacity); // Apply background image if URL exists via custom property if (imageUrl && imageUrl.trim() !== '') { cell.style.setProperty('--bg-image-url', `url('${imageUrl}')`); cell.style.setProperty('--bg-image-opacity', imageOpacity / 100); cell.style.backgroundSize = 'cover'; cell.style.backgroundPosition = 'center center'; cell.style.backgroundRepeat = 'no-repeat'; } else { // Explicitly clear background image custom property cell.style.removeProperty('--bg-image-url'); } // Apply marked text styles const textSpan = cell.querySelector('.bingo-cell-text'); if (textSpan) { const textColor = localStorage.getItem(vars.LS_MARKED_CELL_TEXT_COLOR) || vars.DEFAULT_MARKED_CELL_TEXT_COLOR; const textOpacity = parseInt(localStorage.getItem(vars.LS_MARKED_CELL_TEXT_OPACITY) || vars.DEFAULT_MARKED_CELL_TEXT_OPACITY, 10); const outlineColor = localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_COLOR) || vars.DEFAULT_MARKED_CELL_OUTLINE_COLOR; const outlineOpacity = parseInt(localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_OPACITY) || vars.DEFAULT_MARKED_CELL_OUTLINE_OPACITY, 10); let outlineWidth = parseFloat(localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_WIDTH)); // Default check: // Add log before default check if (isNaN(outlineWidth) || outlineWidth < 0) { outlineWidth = vars.DEFAULT_MARKED_CELL_OUTLINE_WIDTH; } const rgbaTextColor = hexToRgba(textColor, textOpacity); textSpan.style.color = rgbaTextColor; // Override default style if (outlineWidth > 0) { const rgbaOutlineColor = hexToRgba(outlineColor, outlineOpacity); const shadow = ` -${outlineWidth}px -${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px -${outlineWidth}px 0 ${rgbaOutlineColor}, -${outlineWidth}px ${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px ${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px 0 0 ${rgbaOutlineColor}, -${outlineWidth}px 0 0 ${rgbaOutlineColor}, 0 ${outlineWidth}px 0 ${rgbaOutlineColor}, 0 -${outlineWidth}px 0 ${rgbaOutlineColor} `; textSpan.style.textShadow = shadow; // Override default style } else { textSpan.style.textShadow = "none"; // Override default style } } } else { // If unmarked, re-apply default styles (which resets bg color, bg image, border color, and TEXT styles) applyCellStyle(cell); } } // --- Re-apply styles to all currently marked cells --- function refreshMarkedCellStyles() { const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.marked'); markedCells.forEach(cell => { applyMarkedCellStyle(cell); // Re-apply based on current settings }); // After refreshing marked styles, check for wins checkWinConditions(); } // --- Save current header settings to localStorage --- function saveHeaderSettings() { localStorage.setItem(vars.LS_HEADER_TEXT, document.getElementById('header-text-input').value); localStorage.setItem(vars.LS_HEADER_IMAGE_URL, document.getElementById('header-image-url-input').value); localStorage.setItem(vars.LS_HEADER_TEXT_COLOR, document.getElementById('header-text-color-picker').value); localStorage.setItem(vars.LS_HEADER_TEXT_COLOR_OPACITY, document.getElementById('header-text-color-opacity-slider').value); localStorage.setItem(vars.LS_HEADER_TEXT_FONT_SIZE, document.getElementById('header-text-font-size-input').value); // Save font size localStorage.setItem(vars.LS_HEADER_TEXT_FONT_FAMILY, document.getElementById('header-text-font-family-select').value); // Save font family // Save header background localStorage.setItem(vars.LS_HEADER_BG_COLOR, document.getElementById('header-bg-color-picker').value); localStorage.setItem(vars.LS_HEADER_BG_OPACITY, document.getElementById('header-bg-opacity-slider').value); // Save header text outline localStorage.setItem(vars.LS_HEADER_TEXT_OUTLINE_COLOR, document.getElementById('header-text-outline-color-picker').value); localStorage.setItem(vars.LS_HEADER_TEXT_OUTLINE_OPACITY, document.getElementById('header-text-outline-opacity-slider').value); localStorage.setItem(vars.LS_HEADER_TEXT_OUTLINE_WIDTH, document.getElementById('header-text-outline-width-input').value); // Save header font style localStorage.setItem(vars.LS_HEADER_TEXT_STYLE_ITALIC, document.getElementById('header-text-style-italic').checked); localStorage.setItem(vars.LS_HEADER_TEXT_STYLE_BOLD, document.getElementById('header-text-style-bold').checked); } // --- Function to apply header background style --- function applyHeaderBgStyle() { const headerElement = document.getElementById('board-header'); if (!headerElement) return; const bgColor = localStorage.getItem(vars.LS_HEADER_BG_COLOR) || vars.DEFAULT_HEADER_BG_COLOR; const opacity = parseInt(localStorage.getItem(vars.LS_HEADER_BG_OPACITY) || vars.DEFAULT_HEADER_BG_OPACITY, 10); headerElement.style.backgroundColor = hexToRgba(bgColor, opacity); } // --- Function to update the header display --- function updateHeaderDisplay() { // Read text, applying default ONLY if the key is missing let headerText = localStorage.getItem(vars.LS_HEADER_TEXT); if (headerText === null) { // Check if key exists headerText = vars.DEFAULT_HEADER_TEXT; // Apply default only if key missing } // Read image URL: use stored value if key exists, otherwise use default let headerImageUrl = localStorage.getItem(vars.LS_HEADER_IMAGE_URL); if (headerImageUrl === null) { // Check if key exists in localStorage headerImageUrl = vars.DEFAULT_HEADER_IMAGE_URL; // Apply default ONLY if key is missing } const headerContainer = document.getElementById("custom-header-content"); if (!headerContainer) return; // Clear existing content headerContainer.innerHTML = ""; // Get default color and opacity let headerTextColor = localStorage.getItem(vars.LS_HEADER_TEXT_COLOR); if (headerTextColor === null) { // Check if key exists before applying default headerTextColor = vars.DEFAULT_HEADER_TEXT_COLOR; } let headerTextOpacity = localStorage.getItem(vars.LS_HEADER_TEXT_COLOR_OPACITY); if (headerTextOpacity === null) { headerTextOpacity = vars.DEFAULT_HEADER_TEXT_COLOR_OPACITY; } let headerStyleItalic = localStorage.getItem(vars.LS_HEADER_TEXT_STYLE_ITALIC) === 'true'; // Convert string to boolean let headerStyleBold = localStorage.getItem(vars.LS_HEADER_TEXT_STYLE_BOLD) === 'true'; // Convert string to boolean const rgbaDefaultColor = hexToRgba(headerTextColor, parseInt(headerTextOpacity, 10)); // Get outline settings let headerOutlineColor = localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_COLOR); if (headerOutlineColor === null) headerOutlineColor = vars.DEFAULT_HEADER_TEXT_OUTLINE_COLOR; let headerOutlineOpacity = localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_OPACITY); if (headerOutlineOpacity === null) headerOutlineOpacity = vars.DEFAULT_HEADER_TEXT_OUTLINE_OPACITY; let headerOutlineWidth = parseFloat(localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_WIDTH)); if (isNaN(headerOutlineWidth) || headerOutlineWidth < 0) { headerOutlineWidth = vars.DEFAULT_HEADER_TEXT_OUTLINE_WIDTH; } let hasText = headerText && headerText.trim() !== ""; // Determine if we are currently using the default bean URL let isUsingDefaultBeanUrl = headerImageUrl === vars.DEFAULT_HEADER_IMAGE_URL; // Determine if there is a custom image (non-empty URL that isn't the default bean) let hasCustomImage = headerImageUrl && headerImageUrl.trim() !== "" && !isUsingDefaultBeanUrl; // Add text if it exists if (hasText) { const h1 = document.createElement("h1"); // Remove default text-4xl and font-bold, apply font size/family/style/weight via style // h1.className = ""; // Get and apply font size let headerFontSize = parseInt(localStorage.getItem(vars.LS_HEADER_TEXT_FONT_SIZE), 10); if (isNaN(headerFontSize) || headerFontSize <= 0) { headerFontSize = vars.DEFAULT_HEADER_TEXT_FONT_SIZE; } h1.style.fontSize = `${headerFontSize}px`; // Apply font size // Get and apply font family const headerFontFamily = localStorage.getItem(vars.LS_HEADER_TEXT_FONT_FAMILY) || vars.DEFAULT_HEADER_TEXT_FONT_FAMILY; h1.style.fontFamily = headerFontFamily; // Apply font family // Apply font style and weight h1.style.fontStyle = headerStyleItalic ? 'italic' : 'normal'; h1.style.fontWeight = headerStyleBold ? 'bold' : 'normal'; h1.style.color = rgbaDefaultColor; // Apply default color+opacity to H1 // Apply outline to H1 if (headerOutlineWidth > 0) { const rgbaOutlineColor = hexToRgba(headerOutlineColor, parseInt(headerOutlineOpacity, 10)); const shadow = ` -${headerOutlineWidth}px -${headerOutlineWidth}px 0 ${rgbaOutlineColor}, ${headerOutlineWidth}px -${headerOutlineWidth}px 0 ${rgbaOutlineColor}, -${headerOutlineWidth}px ${headerOutlineWidth}px 0 ${rgbaOutlineColor}, ${headerOutlineWidth}px ${headerOutlineWidth}px 0 ${rgbaOutlineColor}, ${headerOutlineWidth}px 0 0 ${rgbaOutlineColor}, -${headerOutlineWidth}px 0 0 ${rgbaOutlineColor}, 0 ${headerOutlineWidth}px 0 ${rgbaOutlineColor}, 0 -${headerOutlineWidth}px 0 ${rgbaOutlineColor} `; h1.style.textShadow = shadow; } else { h1.style.textShadow = "none"; } // Split the header text by spaces, but keep the spaces as separate elements const parts = headerText.trim().split(/(\s+)/); parts.forEach(part => { if (!part) return; // Skip empty parts resulting from split if (part.match(/^\s+$/)) { // If the part is only whitespace, add it as a text node to preserve spacing h1.appendChild(document.createTextNode(part)); } else { // Check if the part matches the Word[color] pattern exactly // Regex: Start (^), Word (\S+?), literal \[, capture color ([^\\\]]+?), literal \], End ($) const tagMatch = part.match(/^(\S+?)\[([^\]]+?)\]$/); if (tagMatch) { const word = tagMatch[1]; const color = tagMatch[2].trim(); // Extract and trim color const span = document.createElement("span"); span.textContent = word; // Only the word part if (color) { // Check if color is not empty after trim span.style.color = color; // Apply specific color } // If color was empty or invalid, it inherits the default from h1 h1.appendChild(span); } else { // Part is just a regular word (or doesn't match the pattern) const span = document.createElement("span"); span.textContent = part; // Add the whole part as is // Inherits default color and outline from h1 h1.appendChild(span); } } }); headerContainer.appendChild(h1); } // Add custom image if URL exists and it's not the default bean URL if (hasCustomImage) { // create a div and an image element const div = document.createElement('div'); const img = document.createElement('img'); div.id = 'header-image-container'; img.id = 'header-image'; img.src = headerImageUrl; img.alt = "Custom Header Image"; img.className = "max-h-16 max-w-full object-contain"; // Adjust size as needed img.onerror = () => { img.remove(); // Remove broken image placeholder showNotification("Could not load custom header image.", "warning"); // No fallback logic here. If custom image fails, it's just gone. }; div.appendChild(img); headerContainer.appendChild(div); } // If there is NO text and NO custom image, AND we ended up using the default bean URL, add the default bean else if (!hasText && !hasCustomImage && isUsingDefaultBeanUrl) { addDefaultBean(headerContainer); } // Implicitly, if there IS text but NO custom image, nothing else is added here. // If the user set the URL to "", hasCustomImage will be false, and isUsingDefaultBeanUrl will be false, so nothing is added. } // Helper to add the default bean image function addDefaultBean(container) { // create a div and an image element const div = document.createElement('div'); const img = document.createElement('img'); div.id = 'bean-container'; img.id = 'bean'; img.src = '/bean.svg'; img.alt = 'Bean'; img.className = 'w-16 h-16 cursor-pointer'; // Use consistent size img.onclick = (event) => explodeBeans(event); div.appendChild(img); container.appendChild(div); } // --- Save current default cell style settings to localStorage --- function saveCellStyleSettings() { localStorage.setItem(vars.LS_CELL_BORDER_COLOR, document.getElementById('cell-border-color-picker').value); localStorage.setItem(vars.LS_CELL_BORDER_OPACITY, document.getElementById('cell-border-opacity-slider').value); localStorage.setItem(vars.LS_CELL_BORDER_WIDTH, document.getElementById('cell-border-width-input').value); localStorage.setItem(vars.LS_CELL_BG_COLOR, document.getElementById('cell-background-color-picker').value); localStorage.setItem(vars.LS_CELL_BG_OPACITY, document.getElementById('cell-background-opacity-slider').value); localStorage.setItem(vars.LS_CELL_BG_IMAGE_URL, document.getElementById('cell-background-image-url-input').value); localStorage.setItem(vars.LS_CELL_BG_IMAGE_OPACITY, document.getElementById('cell-background-image-opacity-slider').value); // Save image opacity // Save text styles localStorage.setItem(vars.LS_CELL_TEXT_COLOR, document.getElementById('cell-text-color-picker').value); localStorage.setItem(vars.LS_CELL_TEXT_OPACITY, document.getElementById('cell-text-opacity-slider').value); localStorage.setItem(vars.LS_CELL_OUTLINE_COLOR, document.getElementById('cell-outline-color-picker').value); localStorage.setItem(vars.LS_CELL_OUTLINE_OPACITY, document.getElementById('cell-outline-opacity-slider').value); localStorage.setItem(vars.LS_CELL_OUTLINE_WIDTH, document.getElementById('cell-outline-width-input').value); } // --- Apply saved default cell styles to a cell --- function applyCellStyle(cell) { if (!cell || cell.classList.contains('marked')) return; // Only apply to non-marked cells // Borders const borderColor = localStorage.getItem(vars.LS_CELL_BORDER_COLOR) || vars.DEFAULT_CELL_BORDER_COLOR; const borderOpacity = parseInt(localStorage.getItem(vars.LS_CELL_BORDER_OPACITY) || vars.DEFAULT_CELL_BORDER_OPACITY, 10); cell.style.borderColor = hexToRgba(borderColor, borderOpacity); // Apply border width let borderWidth = parseInt(localStorage.getItem(vars.LS_CELL_BORDER_WIDTH) || vars.DEFAULT_CELL_BORDER_WIDTH, 10); if (isNaN(borderWidth) || borderWidth < 0) { // Add check for NaN and negative borderWidth = vars.DEFAULT_CELL_BORDER_WIDTH; } cell.style.borderWidth = `${borderWidth}px`; // Background Color / Image const bgColor = localStorage.getItem(vars.LS_CELL_BG_COLOR) || vars.DEFAULT_CELL_BG_COLOR; const bgOpacity = parseInt(localStorage.getItem(vars.LS_CELL_BG_OPACITY) || vars.DEFAULT_CELL_BG_OPACITY, 10); const bgImageUrl = localStorage.getItem(vars.LS_CELL_BG_IMAGE_URL) || vars.DEFAULT_CELL_BG_IMAGE_URL; const bgImageOpacity = parseInt(localStorage.getItem(vars.LS_CELL_BG_IMAGE_OPACITY) || vars.DEFAULT_CELL_BG_IMAGE_OPACITY, 10); cell.style.opacity = ''; // Reset direct opacity if (bgImageUrl) { cell.style.setProperty('--bg-image-url', `url('${bgImageUrl}')`); cell.style.setProperty('--bg-image-opacity', bgImageOpacity / 100); // Set image opacity property cell.style.backgroundColor = hexToRgba(bgColor, bgOpacity); } else { cell.style.removeProperty('--bg-image-url'); // NEW: Remove CSS custom property cell.style.removeProperty('--bg-image-opacity'); // Remove image opacity property cell.style.backgroundColor = hexToRgba(bgColor, bgOpacity); } // Text Styles const textSpan = cell.querySelector('.bingo-cell-text'); if (textSpan) { const textColor = localStorage.getItem(vars.LS_CELL_TEXT_COLOR) || vars.DEFAULT_CELL_TEXT_COLOR; const textOpacity = parseInt(localStorage.getItem(vars.LS_CELL_TEXT_OPACITY) || vars.DEFAULT_CELL_TEXT_OPACITY, 10); const outlineColor = localStorage.getItem(vars.LS_CELL_OUTLINE_COLOR) || vars.DEFAULT_CELL_OUTLINE_COLOR; const outlineOpacity = parseInt(localStorage.getItem(vars.LS_CELL_OUTLINE_OPACITY) || vars.DEFAULT_CELL_OUTLINE_OPACITY, 10); let outlineWidth = parseFloat(localStorage.getItem(vars.LS_CELL_OUTLINE_WIDTH)); if (isNaN(outlineWidth) || outlineWidth < 0) outlineWidth = vars.DEFAULT_CELL_OUTLINE_WIDTH; const rgbaTextColor = hexToRgba(textColor, textOpacity); textSpan.style.color = rgbaTextColor; textSpan.style.setProperty('--cell-text-color', rgbaTextColor); if (outlineWidth > 0) { const rgbaOutlineColor = hexToRgba(outlineColor, outlineOpacity); // Create outline using multiple text shadows const shadow = ` -${outlineWidth}px -${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px -${outlineWidth}px 0 ${rgbaOutlineColor}, -${outlineWidth}px ${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px ${outlineWidth}px 0 ${rgbaOutlineColor}, ${outlineWidth}px 0 0 ${rgbaOutlineColor}, -${outlineWidth}px 0 0 ${rgbaOutlineColor}, 0 ${outlineWidth}px 0 ${rgbaOutlineColor}, 0 -${outlineWidth}px 0 ${rgbaOutlineColor} `; textSpan.style.textShadow = shadow; textSpan.style.setProperty('--cell-text-outline', shadow); } else { textSpan.style.textShadow = 'none'; textSpan.style.removeProperty('--cell-text-outline'); } } } // --- Re-apply default styles to all non-marked cells --- function refreshCellStyles() { const cells = document.querySelectorAll('#bingo-board .bingo-cell:not(.marked)'); cells.forEach(cell => { applyCellStyle(cell); // Re-apply based on current settings }); } // --- Save board background settings to localStorage --- function saveBoardBgSettings() { localStorage.setItem(vars.LS_BOARD_BG_COLOR, document.getElementById('board-bg-color-picker').value); localStorage.setItem(vars.LS_BOARD_BG_IMAGE_URL, document.getElementById('board-bg-image-url-input').value); localStorage.setItem(vars.LS_BOARD_BG_COLOR_OPACITY, document.getElementById('board-bg-color-opacity-slider').value); // Use new ID & key localStorage.setItem(vars.LS_BOARD_BG_IMAGE_OPACITY, document.getElementById('board-bg-image-opacity-slider').value); // NEW } // --- Apply board background style --- function applyBoardBgStyle() { const container = document.getElementById('bingo-board-container'); if (!container) return; const bgColor = localStorage.getItem(vars.LS_BOARD_BG_COLOR) || vars.DEFAULT_BOARD_BG_COLOR; const bgImageUrl = localStorage.getItem(vars.LS_BOARD_BG_IMAGE_URL) || vars.DEFAULT_BOARD_BG_IMAGE_URL; const bgColorOpacity = parseInt(localStorage.getItem(vars.LS_BOARD_BG_COLOR_OPACITY) || vars.DEFAULT_BOARD_BG_COLOR_OPACITY, 10); const bgImageOpacity = parseInt(localStorage.getItem(vars.LS_BOARD_BG_IMAGE_OPACITY) || vars.DEFAULT_BOARD_BG_IMAGE_OPACITY, 10); // NEW // Apply background color directly to the container container.style.backgroundColor = hexToRgba(bgColor, bgColorOpacity); // Apply background image via CSS variables for the pseudo-element if (bgImageUrl && bgImageUrl.trim() !== '') { container.style.setProperty('--board-bg-image-url', `url('${bgImageUrl}')`); container.style.setProperty('--board-bg-image-opacity', bgImageOpacity / 100); } else { container.style.removeProperty('--board-bg-image-url'); container.style.removeProperty('--board-bg-image-opacity'); } } // Helper to save board state function saveBoardState() { const sizeInput = document.getElementById('board-size'); const size = sizeInput ? parseInt(sizeInput.value, 10) : 5; // Default to 5 if input missing const markedIndices = getMarkedIndices(); const originalUserItems = getItemsFromInput(); // Get current text from textarea localStorage.setItem(vars.LS_BOARD_SIZE, size); localStorage.setItem(vars.LS_CELL_ITEMS, JSON.stringify(currentItems)); // Save items potentially on board (padded/sliced) localStorage.setItem(vars.LS_DISPLAYED_ITEMS, JSON.stringify(displayedItems)); // Save displayed items localStorage.setItem(vars.LS_MARKED_INDICES, JSON.stringify(markedIndices)); localStorage.setItem(vars.LS_ORIGINAL_ITEMS, JSON.stringify(originalUserItems)); // Save user's raw input from textarea saveBackgroundSettings(); // Save background settings from inputs saveHeaderSettings(); // Save header settings from inputs saveMarkedStyleSettings(); // Save marked cell style settings saveCellStyleSettings(); // Save default cell style settings saveBoardBgSettings(); // Save board background settings } export function generateBoard() { const sizeInput = document.getElementById('board-size'); const size = parseInt(sizeInput.value, 10); if (isNaN(size) || size < 1) { showNotification("Please enter a valid board size (minimum 1).", 'warning'); return; } sizeInput.value = size; // Ensure value reflects parsed int // Get items from input and store them as the canonical list const originalUserItems = getItemsFromInput(); // localStorage.setItem(vars.LS_ORIGINAL_ITEMS, JSON.stringify(originalUserItems)); // Moved to saveBoardState const requiredItems = size * size; let itemsForBoard = [...originalUserItems]; // Start with a copy of the user's raw input let notificationMessage = 'Board generated successfully!'; let notificationType = 'success'; if (itemsForBoard.length < requiredItems) { const diff = requiredItems - itemsForBoard.length; notificationMessage = `Warning: Needed ${requiredItems} items, found ${itemsForBoard.length}. Padding with ${diff} placeholder(s).`; notificationType = 'warning'; for (let i = 0; i < diff; i++) { itemsForBoard.push(`Placeholder ${i + 1}`); } // currentItems will now hold the padded list for the board currentItems = [...itemsForBoard]; } else if (itemsForBoard.length > requiredItems) { notificationMessage = `Info: Found ${itemsForBoard.length} items, randomly selecting ${requiredItems}.`; notificationType = 'info'; itemsForBoard = shuffleArray(itemsForBoard).slice(0, requiredItems); // currentItems will now hold the selected subset for the board currentItems = [...itemsForBoard]; } else { // If exact number, currentItems are the same as originalUserItems currentItems = [...originalUserItems]; } // Save the items that are actually available for the board (potentially padded/sliced) // localStorage.setItem(vars.LS_CELL_ITEMS, JSON.stringify(currentItems)); // Moved to saveBoardState // Shuffle the items specifically for display (use currentItems which has the right size) displayedItems = shuffleArray([...currentItems]); const board = document.getElementById("bingo-board"); board.innerHTML = ""; // Clear previous board board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`; displayedItems.forEach((item, index) => { const cell = document.createElement("div"); cell.classList.add("bingo-cell", "cursor-pointer"); // cell.textContent = item; // OLD WAY cell.dataset.index = index; // Add index for saving marks cell.onclick = () => selectCell(cell); applyCellStyle(cell); // Apply default styles upon creation // NEW: Create span for text content const textSpan = document.createElement("span"); textSpan.classList.add("bingo-cell-text"); textSpan.textContent = item; cell.appendChild(textSpan); board.appendChild(cell); }); clearMarks(false); // Clear previous marks visually AND their styles, but don't save yet equalizeCellSizes(); // Add this call // saveBoardState(); // REMOVED first call - Save the newly generated board state (size, items, displayed, marks) // Explicitly save background settings before reload - NO, save everything together // saveBackgroundSettings(); // Get the new background value and inject a style override before reload const newBackgroundValue = setBackground(); // Apply and get value let overrideStyle = document.getElementById('background-override-style'); if (!overrideStyle) { overrideStyle = document.createElement('style'); overrideStyle.id = 'background-override-style'; document.head.appendChild(overrideStyle); } // Use !important to try and force application during reload flash overrideStyle.textContent = `html { background: ${newBackgroundValue} !important; background-repeat: no-repeat !important; background-attachment: fixed !important; }`; showNotification(notificationMessage, notificationType); // *** Save the complete final state just before reload *** saveBoardState(); // Includes size, items, displayed, marks, original items, background, header, marked style // Reload the page to ensure correct rendering based on saved state location.reload(); } export function randomizeBoard() { const storedItemsRaw = localStorage.getItem(vars.LS_CELL_ITEMS); const sizeRaw = localStorage.getItem(vars.LS_BOARD_SIZE); if (!storedItemsRaw || !sizeRaw) { showNotification("Cannot randomize: Board state not found. Please generate the board first.", 'warning'); return; } const storedItems = JSON.parse(storedItemsRaw); const size = parseInt(sizeRaw, 10); const requiredItems = size * size; if (storedItems.length < requiredItems) { showNotification(`Cannot randomize: Not enough items saved (${storedItems.length}/${requiredItems}). Please generate the board again.`, 'warning'); return; } // Ensure we use the correct set if placeholders were added or items were sliced let itemsToShuffle = [...storedItems]; if (itemsToShuffle.length > requiredItems) { // This case implies items were sliced during generation, which is stored in currentItems // Randomizing should still use the items that *could* be on the board itemsToShuffle = itemsToShuffle.slice(0, requiredItems); } else if (itemsToShuffle.length < requiredItems) { // This case shouldn't happen if generateBoard saved padded items showNotification("Cannot randomize: Item count mismatch. Please regenerate the board.", 'error'); return; } displayedItems = shuffleArray([...itemsToShuffle]); // Shuffle the items for display const board = document.getElementById("bingo-board"); const cells = board.querySelectorAll(".bingo-cell"); if (cells.length !== displayedItems.length) { showNotification("Cannot randomize: Board size mismatch. Please generate the board again.", 'error'); return; } cells.forEach((cell, index) => { // cell.textContent = displayedItems[index]; // OLD WAY // NEW: Update or create text span let textSpan = cell.querySelector(".bingo-cell-text"); if (!textSpan) { // Should exist, but create if missing textSpan = document.createElement("span"); textSpan.classList.add("bingo-cell-text"); cell.innerHTML = cell.appendChild(textSpan); } textSpan.textContent = displayedItems[index]; cell.classList.remove("marked"); // Clear marks visually applyMarkedCellStyle(cell); // Reset marked styles (which also calls applyCellStyle) }); // Explicitly remove any existing highlights BEFORE clearing the search input const currentlyHighlighted = board.querySelectorAll('.bingo-cell.highlighted'); currentlyHighlighted.forEach(cell => cell.classList.remove('highlighted')); clearSearch(); // Clear search highlights and input clearMarks(false); // Clear visual marks checkWinConditions(); // Check for wins after randomization (should be none) saveBoardState(); // Save the new randomized state (including cleared marks) showNotification('Board randomized!', 'success'); } function selectCell(cell) { cell.classList.toggle("marked"); // Toggle the dedicated 'marked' class applyMarkedCellStyle(cell); // Apply/remove styles based on new state and settings saveBoardState(); // RE-ADD: Save updated marks immediately after click checkWinConditions(); // Check for wins after a cell is selected/deselected } export function clearMarks(save = true) { const cells = document.querySelectorAll('#bingo-board .bingo-cell'); cells.forEach(cell => { if (cell.classList.contains('marked')) { cell.classList.remove('marked'); // Apply default style ONLY to the cell just unmarked applyCellStyle(cell); } cell.classList.remove('win-cell'); // NEW: Remove win highlight }); // Remove win class from all cells, even unmarked ones cells.forEach(cell => cell.classList.remove('win-cell')); if (save) { saveBoardState(); // Save cleared marks showNotification('Marks cleared.', 'info'); } // No need to call checkWinConditions here, as all marks are cleared } // Fisher-Yates (aka Knuth) Shuffle Algorithm function shuffleArray(array) { let currentIndex = array.length, randomIndex; while (currentIndex !== 0) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex]]; } return array; } // --- Function to reset ONLY board content/structure settings --- export function resetSettings() { // Clear localStorage relevant ONLY to the board structure/content localStorage.removeItem(vars.LS_BOARD_SIZE); localStorage.removeItem(vars.LS_CELL_ITEMS); // The items configured for the board (padded/sliced) localStorage.removeItem(vars.LS_DISPLAYED_ITEMS); // The current layout localStorage.removeItem(vars.LS_MARKED_INDICES); localStorage.removeItem(vars.LS_ORIGINAL_ITEMS); // User's raw input // Reset global variables for board content currentItems = []; displayedItems = []; // Reset ONLY board-related form inputs document.getElementById('board-size').value = 5; // Default size document.getElementById('cell-contents').value = ''; document.getElementById('file-input').value = ''; // Clear file input // DO NOT reset background inputs // DO NOT reset header inputs // DO NOT reset marked style inputs // Clear the board display const board = document.getElementById('bingo-board'); board.innerHTML = '
Board Reset. Generate a new board!
'; board.style.gridTemplateColumns = ''; // Clear grid style board.style.gridAutoRows = ''; // Also clear gridAutoRows // Reset container width to default const boardContainer = document.getElementById('bingo-board-container'); const boardHeader = document.getElementById('board-header'); if (boardContainer) boardContainer.style.maxWidth = ''; // Let it resize naturally or via equalize if (boardHeader) boardHeader.style.maxWidth = ''; // Let it resize naturally or via equalize // Call equalize after potentially changing board content/size equalizeCellSizes(); // DO NOT reset background - keep current style // setBackground(); // DO NOT reset header display - keep current style // updateHeaderDisplay(); // DO NOT refresh marked styles - keep current style // refreshMarkedCellStyles(); showNotification('Board settings reset. Generate a new board to apply.', 'success'); } // --- Function to reset ALL application settings --- export function resetAllSettings() { if (confirm('Are you sure you want to reset ALL settings (styles, header, board content, etc.) to their defaults? This cannot be undone.')) { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith('beango_')) { keysToRemove.push(key); } } keysToRemove.forEach(key => { localStorage.removeItem(key); console.log(`Removed setting: ${key}`); // Optional: log removed keys }); showNotification('Resetting all settings to defaults...', 'warning', 1500); // Show brief message before reload // Use a short delay before reload to ensure the notification is visible setTimeout(() => { location.reload(); }, 1600); // Slightly longer than notification duration } } // --- Helper to get indices of marked cells --- function getMarkedIndices() { const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.marked'); const indices = []; markedCells.forEach(cell => { // Ensure dataset.index exists and is a valid number if (cell.dataset && typeof cell.dataset.index !== 'undefined') { const index = parseInt(cell.dataset.index, 10); if (!isNaN(index)) { indices.push(index); } } else { console.warn('Marked cell found without a valid data-index:', cell); } }); return indices; } // --- Clear Search Input and Highlights --- function clearSearch() { const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.value = ''; // Clear the input field // Trigger the input event listener to ensure highlights are removed searchInput.dispatchEvent(new Event('input')); } // It's technically redundant to remove the class here now, as the // event listener triggered above should handle it, but it doesn't hurt. const highlightedCells = document.querySelectorAll('#bingo-board .bingo-cell.highlighted'); highlightedCells.forEach(cell => { cell.classList.remove('highlighted'); // Remove highlight class }); } function loadPaneState(configIsOpen) { const pane = document.getElementById('config-pane'); if (configIsOpen) { pane.classList.remove('config-pane-closed'); pane.classList.add('config-pane-open'); } else { pane.classList.remove('config-pane-open'); pane.classList.add('config-pane-closed'); } } function generateDefaultBoard() { // --- No saved board state, GENERATE DEFAULT board --- console.log('No saved board state found. Generating default 5x5 board.'); const defaultSize = 5; currentItems = [...vars.DEFAULT_SAMPLE_ITEMS]; // Set global variable displayedItems = shuffleArray([...vars.DEFAULT_SAMPLE_ITEMS]); // Set global variable const originalItems = [...vars.DEFAULT_SAMPLE_ITEMS]; // For text area const board = document.getElementById('bingo-board'); board.innerHTML = ''; // Clear placeholder or previous error message board.style.gridTemplateColumns = `repeat(${defaultSize}, minmax(0, 1fr))`; // Update UI inputs to reflect default document.getElementById('board-size').value = defaultSize; document.getElementById('cell-contents').value = originalItems.join('\n'); displayedItems.forEach((item, index) => { const cell = document.createElement("div"); cell.classList.add("bingo-cell", "cursor-pointer"); cell.dataset.index = index; cell.onclick = () => selectCell(cell); const textSpan = document.createElement("span"); textSpan.classList.add("bingo-cell-text"); textSpan.textContent = item; cell.appendChild(textSpan); applyCellStyle(cell); // Apply default styles // No marked cells on default board board.appendChild(cell); }); // Save this default state immediately saveBoardState(); showNotification('Generated default 5x5 board.', 'info'); // --- End GENERATE DEFAULT board --- } function generateBoardFromSavedState(savedDisplayedItems, savedItemsText, savedMarkedIndices, size) { console.log("Found saved board state. Loading..."); // Log loading source try { displayedItems = JSON.parse(savedDisplayedItems); // Restore displayed items array // *** ADDED: Restore currentItems as well *** if (savedItemsText) { try { currentItems = JSON.parse(savedItemsText); if (!Array.isArray(currentItems)) { console.warn('Loaded vars.LS_CELL_ITEMS was not an array, resetting.'); currentItems = []; } } catch (e) { showNotification('Error parsing saved cell item pool (vars.LS_CELL_ITEMS). Board may not randomize correctly.', 'warning'); currentItems = []; // Reset on parse error } } else { // If vars.LS_CELL_ITEMS doesn't exist but vars.LS_DISPLAYED_ITEMS does, it's an inconsistent state. console.warn('Inconsistent state: vars.LS_DISPLAYED_ITEMS exists but vars.LS_CELL_ITEMS is missing. Randomization might fail.'); currentItems = []; } // *** END ADDED section *** } catch (e) { showNotification('Error parsing saved board state. Please generate again.', 'error'); localStorage.removeItem(vars.LS_DISPLAYED_ITEMS); localStorage.removeItem(vars.LS_MARKED_INDICES); localStorage.removeItem(vars.LS_CELL_ITEMS); // Also remove potentially corrupted items displayedItems = []; currentItems = []; // Also reset currentItems document.getElementById('bingo-board').innerHTML = '
Error loading board. Generate New.
'; // Don't return yet, let style loading proceed with defaults } const requiredItems = size * size; // Check if the loaded data is consistent if (!Array.isArray(displayedItems) || displayedItems.length !== requiredItems) { showNotification("Saved board data mismatch. Please generate again.", 'warning'); localStorage.removeItem(vars.LS_DISPLAYED_ITEMS); localStorage.removeItem(vars.LS_MARKED_INDICES); localStorage.removeItem(vars.LS_CELL_ITEMS); displayedItems = []; currentItems = []; document.getElementById('bingo-board').innerHTML = '
Data Mismatch. Generate New.
'; // Don't return yet } else { // --- Populate board from SAVED state --- const board = document.getElementById('bingo-board'); board.innerHTML = ''; // Clear placeholder board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`; const markedIndices = savedMarkedIndices ? JSON.parse(savedMarkedIndices) : []; displayedItems.forEach((item, index) => { const cell = document.createElement("div"); cell.classList.add("bingo-cell", "cursor-pointer"); cell.dataset.index = index; cell.onclick = () => selectCell(cell); const textSpan = document.createElement("span"); textSpan.classList.add("bingo-cell-text"); textSpan.textContent = item; cell.appendChild(textSpan); applyCellStyle(cell); // Apply default styles first if (markedIndices.includes(index)) { cell.classList.add("marked"); applyMarkedCellStyle(cell); // Apply marked styles if needed } board.appendChild(cell); }); } // --- End populate board from SAVED state --- } function restoreBackgroundSettings() { // --- Restore Background Settings --- const savedBackgroundType = localStorage.getItem(vars.LS_BACKGROUND_TYPE) || 'gradient'; // Default to gradient const savedSolidColor = localStorage.getItem(vars.LS_SOLID_COLOR) || vars.DEFAULT_SOLID_COLOR; const savedSolidColorOpacity = localStorage.getItem(vars.LS_SOLID_COLOR_OPACITY) || vars.DEFAULT_SOLID_COLOR_OPACITY; const savedGradientColor1 = localStorage.getItem(vars.LS_GRADIENT_COLOR_1) || vars.DEFAULT_GRADIENT_COLOR_1; const savedGradientColor1Opacity = localStorage.getItem(vars.LS_GRADIENT_COLOR_1_OPACITY) || vars.DEFAULT_GRADIENT_COLOR_1_OPACITY; const savedGradientColor2 = localStorage.getItem(vars.LS_GRADIENT_COLOR_2) || vars.DEFAULT_GRADIENT_COLOR_2; const savedGradientColor2Opacity = localStorage.getItem(vars.LS_GRADIENT_COLOR_2_OPACITY) || vars.DEFAULT_GRADIENT_COLOR_2_OPACITY; const savedGradientDirection = localStorage.getItem(vars.LS_GRADIENT_DIRECTION) || vars.DEFAULT_GRADIENT_DIRECTION; // Set radio button document.querySelector(`input[name="background-type"][value="${savedBackgroundType}"]`).checked = true; // Set input values document.getElementById('background-color-picker').value = savedSolidColor; document.getElementById('background-color-opacity-slider').value = savedSolidColorOpacity; document.getElementById('gradient-color-1').value = savedGradientColor1; document.getElementById('gradient-color-1-opacity-slider').value = savedGradientColor1Opacity; document.getElementById('gradient-color-2').value = savedGradientColor2; document.getElementById('gradient-color-2-opacity-slider').value = savedGradientColor2Opacity; document.getElementById('gradient-direction').value = savedGradientDirection; // Show/hide controls and apply background toggleBackgroundControls(); setBackground(); // --- End Restore Background Settings --- } function restoreHeaderSettings(savedHeaderText, savedHeaderImageUrl, savedHeaderTextColor, savedHeaderTextOpacity, savedHeaderBgColor, savedHeaderBgOpacity, savedHeaderOutlineColor, savedHeaderOutlineOpacity, savedHeaderOutlineWidth, savedHeaderFontSize, savedHeaderFontFamily, savedHeaderStyleItalic, savedHeaderStyleBold) { _restoreInputSetting('header-text-input', savedHeaderText); // if the header image is the default bean, then add the explodeBeans function to the onclick event _restoreInputSetting('header-image-url-input', savedHeaderImageUrl); _restoreColorPickerSetting('header-text-color-picker', savedHeaderTextColor); _restoreOpacitySetting('header-text-color-opacity-slider', 'header-text-color-opacity-value', savedHeaderTextOpacity); // Restore header font size input _restoreInputSetting('header-text-font-size-input', savedHeaderFontSize); // Restore header font family select _restoreInputSetting('header-text-font-family-select', savedHeaderFontFamily); // Restore header background inputs _restoreColorPickerSetting('header-bg-color-picker', savedHeaderBgColor); _restoreOpacitySetting('header-bg-opacity-slider', 'header-bg-opacity-value', savedHeaderBgOpacity); // Restore header text outline inputs _restoreColorPickerSetting('header-text-outline-color-picker', savedHeaderOutlineColor); _restoreOpacitySetting('header-text-outline-opacity-slider', 'header-text-outline-opacity-value', savedHeaderOutlineOpacity); _restoreInputSetting('header-text-outline-width-input', savedHeaderOutlineWidth); // Restore header font style checkboxes _restoreCheckboxSetting('header-text-style-italic', savedHeaderStyleItalic); _restoreCheckboxSetting('header-text-style-bold', savedHeaderStyleBold); applyHeaderBgStyle(); // Apply header background style updateHeaderDisplay(); // Apply the loaded header content } function restoreMarkedStyleSettings(savedMarkedColor, savedMarkedColorOpacity, savedMarkedImageUrl, savedMarkedImageOpacity, savedMarkedBorderColor, savedMarkedBorderOpacity, savedMarkedCellTextColor, savedMarkedCellTextOpacity, savedMarkedCellOutlineColor, savedMarkedCellOutlineOpacity, savedMarkedCellOutlineWidth, savedMarkedBorderWidth) { // Marked Color _restoreColorPickerSetting('marked-color-picker', savedMarkedColor); _restoreOpacitySetting('marked-color-opacity-slider', 'marked-color-opacity-value', savedMarkedColorOpacity); // Marked Image _restoreInputSetting('marked-image-url-input', savedMarkedImageUrl); _restoreOpacitySetting('marked-image-opacity-slider', 'marked-image-opacity-value', savedMarkedImageOpacity); // Marked Border _restoreColorPickerSetting('marked-border-color-picker', savedMarkedBorderColor); _restoreOpacitySetting('marked-border-opacity-slider', 'marked-border-opacity-value', savedMarkedBorderOpacity); // Save marked border width _restoreInputSetting('marked-border-width-input', savedMarkedBorderWidth); // Marked Text Color _restoreColorPickerSetting('marked-cell-text-color-picker', savedMarkedCellTextColor); _restoreOpacitySetting('marked-cell-text-opacity-slider', 'marked-cell-text-opacity-value', savedMarkedCellTextOpacity); // Marked Text Outline _restoreColorPickerSetting('marked-cell-outline-color-picker', savedMarkedCellOutlineColor); _restoreOpacitySetting('marked-cell-outline-opacity-slider', 'marked-cell-outline-opacity-value', savedMarkedCellOutlineOpacity); _restoreInputSetting('marked-cell-outline-width-input', savedMarkedCellOutlineWidth); } function restoreCellStyleSettings(savedCellBorderColor, savedCellBorderOpacity, savedCellBgColor, savedCellBgOpacity, savedCellBgImageUrl, savedCellBgImageOpacity, savedCellTextColor, savedCellTextOpacity, savedCellOutlineColor, savedCellOutlineOpacity, savedCellOutlineWidth, savedCellBorderWidth) { // Border _restoreColorPickerSetting('cell-border-color-picker', savedCellBorderColor); _restoreOpacitySetting('cell-border-opacity-slider', 'cell-border-opacity-value', savedCellBorderOpacity); // Apply border width _restoreInputSetting('cell-border-width-input', savedCellBorderWidth); // Background Color _restoreColorPickerSetting('cell-background-color-picker', savedCellBgColor); _restoreOpacitySetting('cell-background-opacity-slider', 'cell-background-opacity-value', savedCellBgOpacity); // Background Image _restoreInputSetting('cell-background-image-url-input', savedCellBgImageUrl); _restoreOpacitySetting('cell-background-image-opacity-slider', 'cell-background-image-opacity-value', savedCellBgImageOpacity); // Text Color _restoreColorPickerSetting('cell-text-color-picker', savedCellTextColor); _restoreOpacitySetting('cell-text-opacity-slider', 'cell-text-opacity-value', savedCellTextOpacity); // Outline Color _restoreColorPickerSetting('cell-outline-color-picker', savedCellOutlineColor); _restoreOpacitySetting('cell-outline-opacity-slider', 'cell-outline-opacity-value', savedCellOutlineOpacity); // Outline Width _restoreInputSetting('cell-outline-width-input', savedCellOutlineWidth); } function restoreBoardBgSettings(savedBoardBgColor, savedBoardBgColorOpacity, savedBoardBgImageUrl, savedBoardBgImageOpacity) { _restoreColorPickerSetting('board-bg-color-picker', savedBoardBgColor); _restoreInputSetting('board-bg-image-url-input', savedBoardBgImageUrl); _restoreOpacitySetting('board-bg-color-opacity-slider', 'board-bg-color-opacity-value', savedBoardBgColorOpacity); _restoreOpacitySetting('board-bg-image-opacity-slider', 'board-bg-image-opacity-value', savedBoardBgImageOpacity); // NEW applyBoardBgStyle(); // Apply loaded style } function restoreSavedItems(savedDisplayedItems, savedItemsText, savedOriginalItemsText) { // Restore the textarea with the original user input if available (or fallback) // Note: If default board was generated, textarea was already set above. // This part primarily handles the case where board existed but original items might be missing. if (savedDisplayedItems) { // Only run this textarea logic if we didn't just generate default if (savedOriginalItemsText) { try { const originalItemsFromStorage = JSON.parse(savedOriginalItemsText); // Only update textarea if it wasn't populated by default gen if (document.getElementById('cell-contents').value === '') { document.getElementById('cell-contents').value = originalItemsFromStorage.join('\n'); } } catch (e) { showNotification('Error parsing saved original items.', 'error'); localStorage.removeItem(vars.LS_ORIGINAL_ITEMS); // Fallback to cell items if original fails and textarea is empty if (savedItemsText && document.getElementById('cell-contents').value === '') { try { document.getElementById('cell-contents').value = JSON.parse(savedItemsText).join('\n'); } catch { /* ignore inner error */ } } } } else if (savedItemsText && document.getElementById('cell-contents').value === '') { // If no original items saved, use the cell items as fallback for textarea try { document.getElementById('cell-contents').value = JSON.parse(savedItemsText).join('\n'); } catch (e) { // Already handled loading currentItems above, just ensure textarea is cleared on error document.getElementById('cell-contents').value = ''; } } } } // --- Load State on Page Load --- function loadFromLocalStorage() { const savedSize = localStorage.getItem(vars.LS_BOARD_SIZE); // Keep checking for size first const savedItemsText = localStorage.getItem(vars.LS_CELL_ITEMS); const savedDisplayedItems = localStorage.getItem(vars.LS_DISPLAYED_ITEMS); const savedMarkedIndices = localStorage.getItem(vars.LS_MARKED_INDICES); const configIsOpen = localStorage.getItem(vars.LS_CONFIG_OPEN) === "true"; const savedOriginalItemsText = localStorage.getItem(vars.LS_ORIGINAL_ITEMS); // Load header text, applying default ONLY if key is missing let savedHeaderText = localStorage.getItem(vars.LS_HEADER_TEXT); if (savedHeaderText === null) { // Check if key exists savedHeaderText = vars.DEFAULT_HEADER_TEXT; // Apply default only if key missing } let savedHeaderImageUrl = localStorage.getItem(vars.LS_HEADER_IMAGE_URL); if (savedHeaderImageUrl === null) { // Check if key exists in localStorage savedHeaderImageUrl = vars.DEFAULT_HEADER_IMAGE_URL; // Apply default ONLY if key is missing } let savedHeaderTextColor = localStorage.getItem(vars.LS_HEADER_TEXT_COLOR); if (savedHeaderTextColor === null) { savedHeaderTextColor = vars.DEFAULT_HEADER_TEXT_COLOR; } const savedHeaderTextOpacity = localStorage.getItem(vars.LS_HEADER_TEXT_COLOR_OPACITY) || vars.DEFAULT_HEADER_TEXT_COLOR_OPACITY; const savedHeaderBgColor = localStorage.getItem(vars.LS_HEADER_BG_COLOR) || vars.DEFAULT_HEADER_BG_COLOR; const savedHeaderBgOpacity = localStorage.getItem(vars.LS_HEADER_BG_OPACITY) || vars.DEFAULT_HEADER_BG_OPACITY; const savedHeaderOutlineColor = localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_COLOR) || vars.DEFAULT_HEADER_TEXT_OUTLINE_COLOR; const savedHeaderOutlineOpacity = localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_OPACITY) || vars.DEFAULT_HEADER_TEXT_OUTLINE_OPACITY; const savedHeaderOutlineWidth = localStorage.getItem(vars.LS_HEADER_TEXT_OUTLINE_WIDTH) || vars.DEFAULT_HEADER_TEXT_OUTLINE_WIDTH; const savedHeaderFontSize = localStorage.getItem(vars.LS_HEADER_TEXT_FONT_SIZE) || vars.DEFAULT_HEADER_TEXT_FONT_SIZE; // Load font size const savedHeaderFontFamily = localStorage.getItem(vars.LS_HEADER_TEXT_FONT_FAMILY) || vars.DEFAULT_HEADER_TEXT_FONT_FAMILY; // Load font family const savedMarkedColor = localStorage.getItem(vars.LS_MARKED_COLOR) || vars.DEFAULT_MARKED_COLOR; const savedMarkedColorOpacity = localStorage.getItem(vars.LS_MARKED_COLOR_OPACITY) || vars.DEFAULT_MARKED_COLOR_OPACITY; const savedMarkedImageUrl = localStorage.getItem(vars.LS_MARKED_IMAGE_URL) || vars.DEFAULT_MARKED_IMAGE_URL; const savedMarkedImageOpacity = localStorage.getItem(vars.LS_MARKED_IMAGE_OPACITY) || vars.DEFAULT_MARKED_IMAGE_OPACITY; const savedCellBorderColor = localStorage.getItem(vars.LS_CELL_BORDER_COLOR) || vars.DEFAULT_CELL_BORDER_COLOR; const savedCellBorderOpacity = localStorage.getItem(vars.LS_CELL_BORDER_OPACITY) || vars.DEFAULT_CELL_BORDER_OPACITY; const savedCellBorderWidth = localStorage.getItem(vars.LS_CELL_BORDER_WIDTH) || vars.DEFAULT_CELL_BORDER_WIDTH; const savedCellBgColor = localStorage.getItem(vars.LS_CELL_BG_COLOR) || vars.DEFAULT_CELL_BG_COLOR; const savedCellBgOpacity = localStorage.getItem(vars.LS_CELL_BG_OPACITY) || vars.DEFAULT_CELL_BG_OPACITY; const savedCellBgImageUrl = localStorage.getItem(vars.LS_CELL_BG_IMAGE_URL) || vars.DEFAULT_CELL_BG_IMAGE_URL; const savedCellBgImageOpacity = localStorage.getItem(vars.LS_CELL_BG_IMAGE_OPACITY) || vars.DEFAULT_CELL_BG_IMAGE_OPACITY; const savedCellTextColor = localStorage.getItem(vars.LS_CELL_TEXT_COLOR) || vars.DEFAULT_CELL_TEXT_COLOR; const savedCellTextOpacity = localStorage.getItem(vars.LS_CELL_TEXT_OPACITY) || vars.DEFAULT_CELL_TEXT_OPACITY; const savedCellOutlineColor = localStorage.getItem(vars.LS_CELL_OUTLINE_COLOR) || vars.DEFAULT_CELL_OUTLINE_COLOR; const savedCellOutlineOpacity = localStorage.getItem(vars.LS_CELL_OUTLINE_OPACITY) || vars.DEFAULT_CELL_OUTLINE_OPACITY; const savedCellOutlineWidth = localStorage.getItem(vars.LS_CELL_OUTLINE_WIDTH) || vars.DEFAULT_CELL_OUTLINE_WIDTH; const savedMarkedBorderColor = localStorage.getItem(vars.LS_MARKED_BORDER_COLOR) || vars.DEFAULT_MARKED_BORDER_COLOR; const savedMarkedBorderOpacity = localStorage.getItem(vars.LS_MARKED_BORDER_OPACITY) || vars.DEFAULT_MARKED_BORDER_OPACITY; const savedMarkedBorderWidth = localStorage.getItem(vars.LS_MARKED_BORDER_WIDTH) || vars.DEFAULT_MARKED_BORDER_WIDTH; const savedMarkedCellTextColor = localStorage.getItem(vars.LS_MARKED_CELL_TEXT_COLOR) || vars.DEFAULT_MARKED_CELL_TEXT_COLOR; const savedMarkedCellTextOpacity = localStorage.getItem(vars.LS_MARKED_CELL_TEXT_OPACITY) || vars.DEFAULT_MARKED_CELL_TEXT_OPACITY; const savedMarkedCellOutlineColor = localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_COLOR) || vars.DEFAULT_MARKED_CELL_OUTLINE_COLOR; const savedMarkedCellOutlineOpacity = localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_OPACITY) || vars.DEFAULT_MARKED_CELL_OUTLINE_OPACITY; const savedMarkedCellOutlineWidth = localStorage.getItem(vars.LS_MARKED_CELL_OUTLINE_WIDTH) || vars.DEFAULT_MARKED_CELL_OUTLINE_WIDTH; const savedBoardBgColor = localStorage.getItem(vars.LS_BOARD_BG_COLOR) || vars.DEFAULT_BOARD_BG_COLOR; const savedBoardBgColorOpacity = localStorage.getItem(vars.LS_BOARD_BG_COLOR_OPACITY) || vars.DEFAULT_BOARD_BG_COLOR_OPACITY; const savedBoardBgImageUrl = localStorage.getItem(vars.LS_BOARD_BG_IMAGE_URL) || vars.DEFAULT_BOARD_BG_IMAGE_URL; const savedBoardBgImageOpacity = localStorage.getItem(vars.LS_BOARD_BG_IMAGE_OPACITY) || vars.DEFAULT_BOARD_BG_IMAGE_OPACITY; // NEW // Restore Config Pane State loadPaneState(configIsOpen); // Determine board size (use saved, or default 5) const size = parseInt(savedSize || "5", 10); document.getElementById('board-size').value = size; // Update input regardless of board source // Restore Board only if essential data exists if (savedDisplayedItems) { generateBoardFromSavedState(savedDisplayedItems, savedItemsText, savedMarkedIndices, size); } else { generateDefaultBoard(); } restoreBackgroundSettings(); restoreHeaderSettings( savedHeaderText, savedHeaderImageUrl, savedHeaderTextColor, savedHeaderTextOpacity, savedHeaderBgColor, savedHeaderBgOpacity, savedHeaderOutlineColor, savedHeaderOutlineOpacity, savedHeaderOutlineWidth, savedHeaderFontSize, // Pass font size savedHeaderFontFamily, // Pass font family localStorage.getItem(vars.LS_HEADER_TEXT_STYLE_ITALIC) === 'true', // Pass italic style localStorage.getItem(vars.LS_HEADER_TEXT_STYLE_BOLD) === 'true' // Pass bold style ); restoreMarkedStyleSettings( savedMarkedColor, savedMarkedColorOpacity, savedMarkedImageUrl, savedMarkedImageOpacity, savedMarkedBorderColor, savedMarkedBorderOpacity, savedMarkedCellTextColor, savedMarkedCellTextOpacity, savedMarkedCellOutlineColor, savedMarkedCellOutlineOpacity, savedMarkedCellOutlineWidth, savedMarkedBorderWidth ); restoreCellStyleSettings( savedCellBorderColor, savedCellBorderOpacity, savedCellBgColor, savedCellBgOpacity, savedCellBgImageUrl, savedCellBgImageOpacity, savedCellTextColor, savedCellTextOpacity, savedCellOutlineColor, savedCellOutlineOpacity, savedCellOutlineWidth, savedCellBorderWidth ); restoreBoardBgSettings(savedBoardBgColor, savedBoardBgColorOpacity, savedBoardBgImageUrl, savedBoardBgImageOpacity); // Pass new value restoreSavedItems(savedDisplayedItems, savedItemsText, savedOriginalItemsText); } // --- Function to make all cells square and equal size --- function equalizeCellSizes() { const board = document.getElementById("bingo-board"); const cells = board?.querySelectorAll(".bingo-cell"); if (!board || !cells || cells.length === 0) return; let size = 0; const sizeInput = document.getElementById("board-size"); const sizeFromInput = sizeInput ? parseInt(sizeInput.value, 10) : 0; if (sizeFromInput && !isNaN(sizeFromInput) && sizeFromInput > 0) { size = sizeFromInput; } else { try { const gridStyle = window.getComputedStyle(board).gridTemplateColumns; const parts = gridStyle.split(" "); if (parts.length > 0 && parts[0] !== "none") { size = parts.length; } } catch (e) { console.error( "Could not determine grid size from styles or input.", e ); } } if (size <= 0) { // Attempt to get size from a potentially existing grid setup if possible try { const gridStyle = window.getComputedStyle(board).gridTemplateColumns; const parts = gridStyle.split(" "); if (parts.length > 0 && parts[0] !== "none") { size = parts.length; } } catch { /* ignore */ } if (size <= 0) { console.error("Failed to determine grid size."); showNotification("Cannot determine valid grid size. Please generate board again.", "warning"); return; // Cannot proceed } } const gap = 4; // From CSS: .bingo-board { gap: 4px; } const minCellSize = 150; // Minimum width/height for a cell let maxWidthNeeded = 0; let maxHeightNeeded = 0; // 1. Reset grid sizing to allow natural measurement of cell content board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`; // Use fractions initially board.style.gridAutoRows = "auto"; // Auto height // 2. Force reflow to apply the reset styles before measuring void board.offsetHeight; // 3. First pass: Find the max width and max height based on actual rendered content cells.forEach((cell) => { // Reset any explicit cell styles (shouldn't be needed, but safe) cell.style.width = ""; cell.style.height = ""; // Measure the cell's natural dimensions after reflow const width = cell.offsetWidth; const height = cell.offsetHeight; maxWidthNeeded = Math.max(maxWidthNeeded, width); maxHeightNeeded = Math.max(maxHeightNeeded, height); }); // 4. Determine the target size for square cells const targetSize = Math.max(maxWidthNeeded, maxHeightNeeded, minCellSize); // 5. Apply the calculated fixed size back to the grid board.style.gridTemplateColumns = `repeat(${size}, ${targetSize}px)`; board.style.gridAutoRows = `${targetSize}px`; // Make cells square // 6. Calculate total board width needed for the container const containerPadding = 32; // Based on p-4 class on #bingo-board-container (1rem = 16px, left + right = 32px) const totalWidth = targetSize * size + gap * (size - 1) + containerPadding; // 7. Update container max-width const boardContainer = document.getElementById("bingo-board-container"); const boardHeader = document.getElementById("board-header"); if (boardContainer) { boardContainer.style.maxWidth = `${totalWidth}px`; } if (boardHeader) { // Keep header consistent with board container width boardHeader.style.maxWidth = `${totalWidth}px`; } } export function explodeBeans(event) { console.log(event); let container = event.target.parentElement; for (let i = 0; i < 20; i++) { const newBean = document.createElement('img'); newBean.src = '/bean.svg'; newBean.style.position = 'absolute'; newBean.style.width = '25px'; newBean.style.height = '25px'; newBean.style.transition = 'transform 1s ease-out, opacity 1s ease-out'; newBean.style.opacity = '0'; // Initial position at the center of the div container newBean.style.transform = 'translate(12px, -50px) scale(0.5)'; container.appendChild(newBean); // Random position around the original bean const angle = Math.random() * 2 * Math.PI; const distance = Math.random() * 100 + 50; // Trigger the animation setTimeout(() => { newBean.style.opacity = '1'; newBean.style.transform = `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px) scale(1)`; }, 0); // Remove the bean after animation setTimeout(() => { newBean.style.opacity = '0'; setTimeout(() => { container.removeChild(newBean); }, 1000); }, 1000); } } // --- Consolidated Event Listeners --- document.addEventListener('DOMContentLoaded', () => { // Apply shared settings from URL if present before loading tryApplySettingsFromUrl(); // Load state first thing loadFromLocalStorage(); // --- Event Listeners Setup using Helpers --- // Background Controls document.querySelectorAll('input[name="background-type"]').forEach(radio => { radio.addEventListener('change', () => { toggleBackgroundControls(); setBackground(); // Apply the change visually saveBackgroundSettings(); // Save the setting }); }); setupInputListener('background-color-picker', 'input', saveBackgroundSettings, setBackground); setupOpacitySliderListener('background-color-opacity-slider', 'background-color-opacity-value', saveBackgroundSettings, setBackground); setupInputListener('gradient-color-1', 'input', saveBackgroundSettings, setBackground); setupOpacitySliderListener('gradient-color-1-opacity-slider', 'gradient-color-1-opacity-value', saveBackgroundSettings, setBackground); setupInputListener('gradient-color-2', 'input', saveBackgroundSettings, setBackground); setupOpacitySliderListener('gradient-color-2-opacity-slider', 'gradient-color-2-opacity-value', saveBackgroundSettings, setBackground); setupInputListener('gradient-direction', 'change', saveBackgroundSettings, setBackground); // Header Controls setupInputListener('header-text-input', 'input', saveHeaderSettings, updateHeaderDisplay); setupInputListener('header-image-url-input', 'input', saveHeaderSettings, updateHeaderDisplay); setupInputListener('header-text-color-picker', 'input', saveHeaderSettings, updateHeaderDisplay); setupOpacitySliderListener('header-text-color-opacity-slider', 'header-text-color-opacity-value', saveHeaderSettings, updateHeaderDisplay); setupInputListener('header-text-font-size-input', 'input', saveHeaderSettings, updateHeaderDisplay); // Add listener for font size setupInputListener('header-text-font-family-select', 'change', saveHeaderSettings, updateHeaderDisplay); // Add listener for font family setupInputListener('header-bg-color-picker', 'input', saveHeaderSettings, applyHeaderBgStyle); setupOpacitySliderListener('header-bg-opacity-slider', 'header-bg-opacity-value', saveHeaderSettings, applyHeaderBgStyle); // Header Font Style Listeners setupInputListener('header-text-style-italic', 'change', saveHeaderSettings, updateHeaderDisplay); setupInputListener('header-text-style-bold', 'change', saveHeaderSettings, updateHeaderDisplay); // Header Text Outline Listeners setupInputListener('header-text-outline-color-picker', 'input', saveHeaderSettings, updateHeaderDisplay); setupOpacitySliderListener('header-text-outline-opacity-slider', 'header-text-outline-opacity-value', saveHeaderSettings, updateHeaderDisplay); setupInputListener('header-text-outline-width-input', 'input', saveHeaderSettings, updateHeaderDisplay); // Marked Style Controls setupInputListener('marked-color-picker', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupOpacitySliderListener('marked-color-opacity-slider', 'marked-color-opacity-value', saveMarkedStyleSettings, refreshMarkedCellStyles); setupInputListener('marked-image-url-input', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupOpacitySliderListener('marked-image-opacity-slider', 'marked-image-opacity-value', saveMarkedStyleSettings, refreshMarkedCellStyles); setupInputListener('marked-border-color-picker', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupOpacitySliderListener('marked-border-opacity-slider', 'marked-border-opacity-value', saveMarkedStyleSettings, refreshMarkedCellStyles); // Marked Text Styles setupInputListener('marked-cell-text-color-picker', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupOpacitySliderListener('marked-cell-text-opacity-slider', 'marked-cell-text-opacity-value', saveMarkedStyleSettings, refreshMarkedCellStyles); setupInputListener('marked-border-width-input', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupInputListener('marked-cell-outline-color-picker', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); setupOpacitySliderListener('marked-cell-outline-opacity-slider', 'marked-cell-outline-opacity-value', saveMarkedStyleSettings, refreshMarkedCellStyles); setupInputListener('marked-cell-outline-width-input', 'input', saveMarkedStyleSettings, refreshMarkedCellStyles); // Default Cell Style Controls setupInputListener('cell-border-color-picker', 'input', saveCellStyleSettings, refreshCellStyles); setupOpacitySliderListener('cell-border-opacity-slider', 'cell-border-opacity-value', saveCellStyleSettings, refreshCellStyles); setupInputListener('cell-border-width-input', 'input', saveCellStyleSettings, refreshCellStyles); setupInputListener('cell-background-color-picker', 'input', saveCellStyleSettings, refreshCellStyles); setupOpacitySliderListener('cell-background-opacity-slider', 'cell-background-opacity-value', saveCellStyleSettings, refreshCellStyles); setupInputListener('cell-background-image-url-input', 'input', saveCellStyleSettings, refreshCellStyles); setupOpacitySliderListener('cell-background-image-opacity-slider', 'cell-background-image-opacity-value', saveCellStyleSettings, refreshCellStyles); // Default Text Styles setupInputListener('cell-text-color-picker', 'input', saveCellStyleSettings, refreshCellStyles); setupOpacitySliderListener('cell-text-opacity-slider', 'cell-text-opacity-value', saveCellStyleSettings, refreshCellStyles); setupInputListener('cell-outline-color-picker', 'input', saveCellStyleSettings, refreshCellStyles); setupOpacitySliderListener('cell-outline-opacity-slider', 'cell-outline-opacity-value', saveCellStyleSettings, refreshCellStyles); setupInputListener('cell-outline-width-input', 'input', saveCellStyleSettings, refreshCellStyles); // Board Background Controls setupInputListener('board-bg-color-picker', 'input', saveBoardBgSettings, applyBoardBgStyle); setupInputListener('board-bg-image-url-input', 'input', saveBoardBgSettings, applyBoardBgStyle); setupOpacitySliderListener('board-bg-color-opacity-slider', 'board-bg-color-opacity-value', saveBoardBgSettings, applyBoardBgStyle); setupOpacitySliderListener('board-bg-image-opacity-slider', 'board-bg-image-opacity-value', saveBoardBgSettings, applyBoardBgStyle); // NEW // --- Search Listener --- const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', () => { const searchTerm = searchInput.value.trim().toLowerCase(); const cells = document.querySelectorAll('#bingo-board .bingo-cell'); const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 0); cells.forEach(cell => { const textSpan = cell.querySelector('.bingo-cell-text'); let isMatch = false; if (textSpan && searchWords.length > 0) { const cellText = textSpan.textContent.trim().toLowerCase(); isMatch = searchWords.every(word => cellText.includes(word)); } cell.classList.toggle('highlighted', isMatch); // More concise toggle }); }); clearSearch(); // Clear search on initial load after setting up listener } // --- Other Listeners --- window.addEventListener('resize', equalizeCellSizes); equalizeCellSizes(); // <-- MOVED HERE clearSearch(); // Clear search highlights and input // Apply marked styles *after* all other styles/settings are restored refreshMarkedCellStyles(); // <-- Keep here // Equalize sizes as the very last step after all content and styles are set equalizeCellSizes(); // <-- MOVED HERE // Check for win conditions after loading everything checkWinConditions(); }); // --- Function to check win conditions --- function checkWinConditions() { const board = document.getElementById('bingo-board'); if (!board) return; const cells = board.querySelectorAll('.bingo-cell'); if (cells.length === 0) return; // No cells to check const size = parseInt(localStorage.getItem(vars.LS_BOARD_SIZE) || '5', 10); const markedIndices = getMarkedIndices(); // Get the indices of marked cells if (markedIndices.length < size) { // Not enough marks for a win // Ensure no win class remains if marks are removed below threshold cells.forEach(cell => cell.classList.remove('win-cell')); return; } const winningIndices = new Set(); // Create a representation of the board (e.g., 2D array or set) const markedSet = new Set(markedIndices); // Check Rows for (let r = 0; r < size; r++) { let rowWin = true; const rowIndices = []; for (let c = 0; c < size; c++) { const index = r * size + c; rowIndices.push(index); if (!markedSet.has(index)) { rowWin = false; break; } } if (rowWin) { rowIndices.forEach(idx => winningIndices.add(idx)); } } // Check Columns for (let c = 0; c < size; c++) { let colWin = true; const colIndices = []; for (let r = 0; r < size; r++) { const index = r * size + c; colIndices.push(index); if (!markedSet.has(index)) { colWin = false; break; } } if (colWin) { colIndices.forEach(idx => winningIndices.add(idx)); } } // Check Diagonal (Top-Left to Bottom-Right) let diag1Win = true; const diag1Indices = []; for (let i = 0; i < size; i++) { const index = i * size + i; diag1Indices.push(index); if (!markedSet.has(index)) { diag1Win = false; break; } } if (diag1Win) { diag1Indices.forEach(idx => winningIndices.add(idx)); } // Check Diagonal (Top-Right to Bottom-Left) let diag2Win = true; const diag2Indices = []; for (let i = 0; i < size; i++) { const index = i * size + (size - 1 - i); diag2Indices.push(index); if (!markedSet.has(index)) { diag2Win = false; break; } } if (diag2Win) { diag2Indices.forEach(idx => winningIndices.add(idx)); } // Apply 'win-cell' class cells.forEach((cell, index) => { if (winningIndices.has(index)) { cell.classList.add('win-cell'); } else { cell.classList.remove('win-cell'); } }); } // --- Settings Import/Export & Sharing --- // v1 fallback: base64url helpers (kept for backward compatibility) function base64UrlEncode(text) { const utf8 = new TextEncoder().encode(text); let binary = ''; utf8.forEach(byte => binary += String.fromCharCode(byte)); const base64 = btoa(binary); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function base64UrlDecode(text) { let base64 = text.replace(/-/g, '+').replace(/_/g, '/'); while (base64.length % 4) base64 += '='; const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return new TextDecoder().decode(bytes); } // v2: Lightweight LZ-based compression for URI component // Adapted from LZ-String (MIT) compressToEncodedURIComponent/decompressFromEncodedURIComponent const LZ = (function() { const f = String.fromCharCode; const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; const baseReverseDic = {}; function getBaseValue(alphabet, character) { if (!baseReverseDic[alphabet]) { baseReverseDic[alphabet] = {}; for (let i = 0; i < alphabet.length; i++) { baseReverseDic[alphabet][alphabet.charAt(i)] = i; } } return baseReverseDic[alphabet][character]; } function compressToEncodedURIComponent(input) { if (input == null) return ''; return _compress(input, 6, function(a){ return keyStrUriSafe.charAt(a); }); } function _compress(uncompressed, bitsPerChar, getCharFromInt) { if (uncompressed == null) return ''; let i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = '', context_wc = '', context_w = '', context_enlargeIn = 2, // Compensate for the first entry which should not count context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii; for (ii = 0; ii < uncompressed.length; ii += 1) { context_c = uncompressed.charAt(ii); if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) { context_dictionary[context_c] = context_dictSize++; context_dictionaryToCreate[context_c] = true; } context_wc = context_w + context_c; if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) { context_w = context_wc; } else { if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { if (context_w.charCodeAt(0) < 256) { for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } } value = context_w.charCodeAt(0); for (i = 0; i < 8; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } else { value = 1; for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1) | value; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = 0; } value = context_w.charCodeAt(0); for (i = 0; i < 16; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } delete context_dictionaryToCreate[context_w]; } else { value = context_dictionary[context_w]; for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } // Add wc to the dictionary. context_dictionary[context_wc] = context_dictSize++; context_w = String(context_c); } } // Output the code for w. if (context_w !== '') { if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { if (context_w.charCodeAt(0) < 256) { for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } } value = context_w.charCodeAt(0); for (i = 0; i < 8; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } else { value = 1; for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1) | value; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = 0; } value = context_w.charCodeAt(0); for (i = 0; i < 16; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } delete context_dictionaryToCreate[context_w]; } else { value = context_dictionary[context_w]; for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } } // Mark the end of the stream value = 2; for (i = 0; i < context_numBits; i++) { context_data_val = (context_data_val << 1) | (value & 1); if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } // Flush the last char while (true) { context_data_val = (context_data_val << 1); if (context_data_position == bitsPerChar - 1) { context_data.push(getCharFromInt(context_data_val)); break; } else { context_data_position++; } } return context_data.join(''); } function decompressFromEncodedURIComponent(input) { if (input == null) return ''; if (input === '') return null; input = input.replace(/ /g, '+'); return _decompress(input.length, 32, function(index) { return getBaseValue(keyStrUriSafe, input.charAt(index)); }); } function _decompress(length, resetValue, getNextValue) { let dictionary = [] , next , enlargeIn = 4 , dictSize = 4 , numBits = 3 , entry = '' , result = [] , i , w , bits, resb, maxpower, power , c , data = {val:getNextValue(0), position:resetValue, index:1}; for (i = 0; i < 3; i += 1) { dictionary[i] = i; } bits = 0; maxpower = Math.pow(2, 2); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } switch (next = bits) { case 0: bits = 0; maxpower = Math.pow(2, 8); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } c = f(bits); break; case 1: bits = 0; maxpower = Math.pow(2, 16); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } c = f(bits); break; case 2: return ''; } dictionary[3] = c; w = c; result.push(c); while (true) { if (data.index > length) { return ''; } bits = 0; maxpower = Math.pow(2, numBits); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } switch (c = bits) { case 0: bits = 0; maxpower = Math.pow(2, 8); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } dictionary[dictSize++] = f(bits); c = dictSize - 1; enlargeIn--; break; case 1: bits = 0; maxpower = Math.pow(2, 16); power = 1; while (power != maxpower) { resb = data.val & data.position; data.position >>= 1; if (data.position == 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb>0 ? 1 : 0) * power; power <<= 1; } dictionary[dictSize++] = f(bits); c = dictSize - 1; enlargeIn--; break; case 2: return result.join(''); } if (enlargeIn == 0) { enlargeIn = Math.pow(2, numBits); numBits++; } if (dictionary[c]) { entry = dictionary[c]; } else { if (c === dictSize) { entry = w + w.charAt(0); } else { return ''; } } result.push(entry); // Add w+entry[0] dictionary[dictSize++] = w + entry.charAt(0); enlargeIn--; w = entry; if (enlargeIn == 0) { enlargeIn = Math.pow(2, numBits); numBits++; } } } return { compressToEncodedURIComponent, decompressFromEncodedURIComponent, }; })(); function collectAllBeangoSettings() { const settings = {}; const prefix = 'beango_'; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { settings[key] = localStorage.getItem(key); } } return settings; } export async function copyShareLink() { try { const data = collectAllBeangoSettings(); const json = JSON.stringify(data); // v2 compressed, URL-safe const encoded = LZ.compressToEncodedURIComponent(json); const shareUrl = `${location.origin}${location.pathname}#s=v2:${encoded}`; let copySucceeded = false; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(shareUrl); copySucceeded = true; } } catch { // Fall through to legacy copy } if (!copySucceeded) { const tempInput = document.createElement('input'); tempInput.value = shareUrl; tempInput.style.position = 'fixed'; tempInput.style.top = '-1000px'; document.body.appendChild(tempInput); tempInput.focus(); tempInput.select(); try { copySucceeded = document.execCommand('copy'); } catch { copySucceeded = false; } document.body.removeChild(tempInput); } if (copySucceeded) { showNotification('Share link copied to clipboard!', 'success'); } else { try { window.prompt('Copy this share link:', shareUrl); } catch { /* ignore */ } showNotification('Could not copy automatically. Link shown for manual copy.', 'warning'); } } catch (e) { console.error('Failed to create share link', e); showNotification('Failed to create share link.', 'error'); } } let _sharedSettingsAppliedOnce = false; function tryApplySettingsFromUrl() { try { const hash = window.location.hash || ''; const match = hash.match(/^#(?:s|settings)=(.+)$/); if (!match) return; const payload = match[1]; let json = ''; if (payload.startsWith('v2:')) { const body = payload.slice(3); json = LZ.decompressFromEncodedURIComponent(body) || ''; } else { // v1 fallback (base64url) json = base64UrlDecode(payload); } const parsed = JSON.parse(json); if (typeof parsed !== 'object' || parsed === null) return; // Clear existing beango_ keys to avoid stale settings const prefix = 'beango_'; const toRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) toRemove.push(key); } toRemove.forEach(k => localStorage.removeItem(k)); let imported = 0; Object.keys(parsed).forEach(key => { if (key.startsWith(prefix)) { const value = parsed[key]; if (value !== undefined && value !== null) { localStorage.setItem(key, String(value)); imported++; } } }); if (imported > 0) { _sharedSettingsAppliedOnce = true; // Clean the URL hash and reload once to ensure all UI reflects new settings if (history && history.replaceState) { history.replaceState(null, '', `${location.pathname}${location.search}`); } else { window.location.hash = ''; } // Reload synchronously; after reload the hash is clean, so this runs only once location.reload(); return; } } catch (e) { console.warn('Failed to apply settings from URL', e); showNotification('Invalid share link.', 'warning'); } } export function exportSettings() { const settingsToExport = {}; const prefix = 'beango_'; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(prefix)) { try { settingsToExport[key] = JSON.parse(localStorage.getItem(key)); } catch (e) { settingsToExport[key] = localStorage.getItem(key); } } } if (Object.keys(settingsToExport).length === 0) { showNotification('No settings found to export.', 'warning'); return; } const settingsJson = JSON.stringify(settingsToExport, null, 2); // Pretty print JSON const blob = new Blob([settingsJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; // Suggest a filename const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-'); // YYYY-MM-DD-HH-MM-SS a.download = `beango-settings-${timestamp}.json`; // Append the link to the body (required for Firefox), click it, and remove it document.body.appendChild(a); a.click(); document.body.removeChild(a); // Release the object URL URL.revokeObjectURL(url); showNotification('Settings export download started.', 'success'); // Keep the textarea update as a visual confirmation/alternative const textarea = document.getElementById('settings-json'); if (textarea) { textarea.value = settingsJson; } } export function importSettings() { // Create a temporary file input element const tempInput = document.createElement('input'); tempInput.type = 'file'; tempInput.accept = '.json,.txt'; // Accept JSON or text files tempInput.style.display = 'none'; // Hide the element tempInput.addEventListener('change', function (event) { const file = event.target.files[0]; if (!file) { return; // No file selected } const reader = new FileReader(); reader.onload = function (e) { const fileContent = e.target.result; let parsedSettings; try { parsedSettings = JSON.parse(fileContent); if (typeof parsedSettings !== 'object' || parsedSettings === null) { throw new Error('Imported file does not contain a valid JSON object.'); } } catch (err) { showNotification(`Error parsing settings file: ${err.message}`, 'error'); return; } // Confirmation before overwriting if (confirm('Importing settings from this file will OVERWRITE your current settings and reload the page. Are you sure?')) { const prefix = 'beango_'; const keysToRemove = []; // 1. Clear existing settings for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(prefix)) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); // 2. Import new settings let importCount = 0; for (const key in parsedSettings) { if (key.startsWith(prefix) && parsedSettings.hasOwnProperty(key) && parsedSettings[key] !== undefined && parsedSettings[key] !== null) { const value = parsedSettings[key]; const valueToStore = (typeof value === 'object') ? JSON.stringify(value) : String(value); localStorage.setItem(key, valueToStore); importCount++; } else { console.warn(`Skipping import for invalid key or value: ${key}`); } } showNotification(`Successfully imported ${importCount} settings from ${file.name}. Reloading...`, 'success', 1500); setTimeout(() => { location.reload(); }, 1600); } }; reader.onerror = function () { showNotification('Error reading the selected file.', 'error'); }; reader.readAsText(file); // Read the file as text // Clean up the temporary input element document.body.removeChild(tempInput); }); // Append to body and trigger click to open file dialog document.body.appendChild(tempInput); tempInput.click(); }