/** * Modal Management Module * Handles DiceModal and SettingsModal ES6 classes with state management */ import { getContext } from '../../../../../../extensions.js'; import { extensionSettings, lastGeneratedData, committedTrackerData, $infoBoxContainer, $thoughtsContainer, $userStatsContainer, clearThoughtBasedExpressionPortraits, setPendingDiceRoll, getPendingDiceRoll, clearSessionAvatarPrompts } from '../../core/state.js'; import { saveSettings, saveChatData } from '../../core/persistence.js'; import { renderUserStats } from '../rendering/userStats.js'; import { renderInfoBox } from '../rendering/infoBox.js'; import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js'; import { renderQuests } from '../rendering/quests.js'; import { renderInventory } from '../rendering/inventory.js'; import { renderEquipment } from '../rendering/equipment.js'; import { rollDice as rollDiceCore, clearDiceRoll as clearDiceRollCore, updateDiceDisplay as updateDiceDisplayCore, addDiceQuickReply as addDiceQuickReplyCore } from '../features/dice.js'; import { i18n } from '../../core/i18n.js'; /** * Modern DiceModal ES6 Class * Manages dice roller modal with proper state management and CSS classes */ export class DiceModal { constructor() { this.modal = document.getElementById('rpg-dice-popup'); this.animation = document.getElementById('rpg-dice-animation'); this.result = document.getElementById('rpg-dice-result'); this.resultValue = document.getElementById('rpg-dice-result-value'); this.resultDetails = document.getElementById('rpg-dice-result-details'); this.rollBtn = document.getElementById('rpg-dice-roll-btn'); this.state = 'IDLE'; // IDLE, ROLLING, SHOWING_RESULT this.isAnimating = false; } /** * Opens the modal with proper animation */ open() { if (this.isAnimating) return; // Apply theme const theme = extensionSettings.theme; this.modal.setAttribute('data-theme', theme); // Apply custom theme if needed if (theme === 'custom') { this._applyCustomTheme(); } // Reset to initial state this._setState('IDLE'); // Open modal with CSS class this.modal.classList.add('is-open'); this.modal.classList.remove('is-closing'); // Focus management this.modal.querySelector('#rpg-dice-popup-close')?.focus(); } /** * Closes the modal with animation */ close() { if (this.isAnimating) return; this.isAnimating = true; this.modal.classList.add('is-closing'); this.modal.classList.remove('is-open'); // Wait for animation to complete setTimeout(() => { this.modal.classList.remove('is-closing'); this.isAnimating = false; // Clear pending roll setPendingDiceRoll(null); }, 200); } /** * Starts the rolling animation */ startRolling() { this._setState('ROLLING'); } /** * Shows the result * @param {number} total - The total roll value * @param {Array} rolls - Individual roll values */ showResult(total, rolls) { this._setState('SHOWING_RESULT'); // Update result values this.resultValue.textContent = total; this.resultValue.classList.add('is-animating'); // Remove animation class after it completes setTimeout(() => { this.resultValue.classList.remove('is-animating'); }, 500); // Show details if multiple rolls if (rolls && rolls.length > 1) { this.resultDetails.textContent = `Rolls: ${rolls.join(', ')}`; } else { this.resultDetails.textContent = ''; } } /** * Manages modal state changes * @private */ _setState(newState) { this.state = newState; switch (newState) { case 'IDLE': this.rollBtn.hidden = false; this.animation.hidden = true; this.result.hidden = true; break; case 'ROLLING': this.rollBtn.hidden = true; this.animation.hidden = false; this.result.hidden = true; this.animation.setAttribute('aria-busy', 'true'); break; case 'SHOWING_RESULT': this.rollBtn.hidden = true; this.animation.hidden = true; this.result.hidden = false; this.animation.setAttribute('aria-busy', 'false'); break; } } /** * Applies custom theme colors * @private */ _applyCustomTheme() { const content = this.modal.querySelector('.rpg-dice-popup-content'); if (content && extensionSettings.customColors) { content.style.setProperty('--rpg-bg', extensionSettings.customColors.bg); content.style.setProperty('--rpg-accent', extensionSettings.customColors.accent); content.style.setProperty('--rpg-text', extensionSettings.customColors.text); content.style.setProperty('--rpg-highlight', extensionSettings.customColors.highlight); } } } /** * SettingsModal - Manages the settings popup modal * Handles opening, closing, theming, and animations */ export class SettingsModal { constructor() { this.modal = document.getElementById('rpg-settings-popup'); this.content = this.modal?.querySelector('.rpg-settings-popup-content'); this.isAnimating = false; } /** * Opens the modal with proper animation */ open() { if (this.isAnimating || !this.modal) return; // Apply theme const theme = extensionSettings.theme || 'default'; this.modal.setAttribute('data-theme', theme); // Apply custom theme if needed if (theme === 'custom') { this._applyCustomTheme(); } // Open modal with CSS class this.modal.classList.add('is-open'); this.modal.classList.remove('is-closing'); // Focus management this.modal.querySelector('#rpg-close-settings')?.focus(); } /** * Closes the modal with animation */ close() { if (this.isAnimating || !this.modal) return; this.isAnimating = true; this.modal.classList.add('is-closing'); this.modal.classList.remove('is-open'); // Wait for animation to complete setTimeout(() => { this.modal.classList.remove('is-closing'); this.isAnimating = false; }, 200); } /** * Updates the theme in real-time (used when theme selector changes) */ updateTheme() { if (!this.modal) return; const theme = extensionSettings.theme || 'default'; this.modal.setAttribute('data-theme', theme); if (theme === 'custom') { this._applyCustomTheme(); } else { // Clear custom CSS variables to let theme CSS take over this._clearCustomTheme(); } } /** * Applies custom theme colors * @private */ _applyCustomTheme() { if (!this.content || !extensionSettings.customColors) return; this.content.style.setProperty('--rpg-bg', extensionSettings.customColors.bg); this.content.style.setProperty('--rpg-accent', extensionSettings.customColors.accent); this.content.style.setProperty('--rpg-text', extensionSettings.customColors.text); this.content.style.setProperty('--rpg-highlight', extensionSettings.customColors.highlight); } /** * Clears custom theme colors * @private */ _clearCustomTheme() { if (!this.content) return; this.content.style.setProperty('--rpg-bg', ''); this.content.style.setProperty('--rpg-accent', ''); this.content.style.setProperty('--rpg-text', ''); this.content.style.setProperty('--rpg-highlight', ''); } } // Global instances let diceModal = null; let settingsModal = null; /** * Sets up the dice roller functionality. * @returns {DiceModal} The initialized DiceModal instance */ export function setupDiceRoller() { // Initialize DiceModal instance diceModal = new DiceModal(); // Click dice display to open popup $('#rpg-dice-display').on('click', function() { openDicePopup(); }); // Close popup - handle both close button and backdrop clicks $('#rpg-dice-popup-close').on('click', function() { closeDicePopup(); }); // Close on backdrop click (clicking outside content) $('#rpg-dice-popup').on('click', function(e) { if (e.target === this) { closeDicePopup(); } }); // Roll dice button $('#rpg-dice-roll-btn').on('click', async function() { await rollDiceCore(diceModal); }); // Save roll button (closes popup and saves the roll) $('#rpg-dice-save-btn').on('click', function() { // Save the pending roll const roll = getPendingDiceRoll(); if (roll) { extensionSettings.lastDiceRoll = roll; saveSettings(); updateDiceDisplayCore(); setPendingDiceRoll(null); } closeDicePopup(); }); // Reset on Enter key $('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) { if (e.which === 13) { rollDiceCore(diceModal); } }); // Clear dice roll button $('#rpg-clear-dice').on('click', function(e) { e.stopPropagation(); // Prevent opening the dice popup clearDiceRollCore(); }); $('#rpg-clear-dice').attr('title', i18n.getTranslation('template.mainPanel.clearLastRoll') || 'Clear last roll'); return diceModal; } /** * Sets up the settings popup functionality. * @returns {SettingsModal} The initialized SettingsModal instance */ export function setupSettingsPopup() { // Initialize SettingsModal instance settingsModal = new SettingsModal(); // Open settings popup $('#rpg-open-settings').on('click', function() { openSettingsPopup(); }); // Close settings popup - close button $('#rpg-close-settings').on('click', function() { closeSettingsPopup(); }); // Close on backdrop click (clicking outside content) $('#rpg-settings-popup').on('click', function(e) { if (e.target === this) { closeSettingsPopup(); } }); // Clear cache button $('#rpg-clear-cache').on('click', function() { // console.log('[RPG Companion] Clear Cache button clicked'); // Clear the data (set to null so panels show "not generated yet") lastGeneratedData.userStats = null; lastGeneratedData.infoBox = null; lastGeneratedData.characterThoughts = null; lastGeneratedData.html = null; // Clear committed tracker data (used for generation context) committedTrackerData.userStats = null; committedTrackerData.infoBox = null; committedTrackerData.characterThoughts = null; // Clear session avatar prompts clearSessionAvatarPrompts(); clearThoughtBasedExpressionPortraits(); // Clear chat metadata immediately (don't wait for debounced save) const context = getContext(); if (context.chat_metadata && context.chat_metadata.rpg_companion) { delete context.chat_metadata.rpg_companion; // console.log('[RPG Companion] Cleared chat_metadata.rpg_companion for current chat'); } // Clear all message swipe data const chat = context.chat; if (chat && chat.length > 0) { for (let i = 0; i < chat.length; i++) { const message = chat[i]; if (message.extra && message.extra.rpg_companion_swipes) { delete message.extra.rpg_companion_swipes; // console.log('[RPG Companion] Cleared swipe data from message at index', i); } if (Array.isArray(message.swipe_info)) { for (const swipeInfo of message.swipe_info) { if (swipeInfo?.extra?.rpg_companion_swipes) { delete swipeInfo.extra.rpg_companion_swipes; } } } } } // Clear the UI if ($infoBoxContainer) { $infoBoxContainer.empty(); } if ($thoughtsContainer) { $thoughtsContainer.empty(); } if ($userStatsContainer) { $userStatsContainer.empty(); } // Reset user stats to default object structure (extensionSettings stores as object, not JSON string) extensionSettings.userStats = { health: 100, satiety: 100, energy: 100, hygiene: 100, arousal: 0, mood: '😐', conditions: 'None', skills: [], inventory: { version: 2, onPerson: "None", clothing: "None", stored: {}, assets: "None" } }; // Reset info box to defaults (as object) extensionSettings.infoBox = { date: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }), weather: '☀️ Clear skies', temperature: '20°C', time: '00:00 - 00:00', location: 'Unknown Location', recentEvents: [] }; // Reset character thoughts to empty (as object) extensionSettings.characterThoughts = { characters: [] }; // Reset classic stats (attributes) to defaults extensionSettings.classicStats = { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 }; // Clear dice roll extensionSettings.lastDiceRoll = null; // Reset level to 1 extensionSettings.level = 1; // Clear quests extensionSettings.quests = { main: "None", optional: [] }; // Clear all locked items extensionSettings.lockedItems = { stats: [], skills: [], inventory: { onPerson: [], clothing: [], stored: {}, assets: [] }, quests: { main: false, optional: [] }, infoBox: { date: false, weather: false, temperature: false, time: false, location: false, recentEvents: false }, characters: {} }; // Save everything saveChatData(); saveSettings(); // Re-render all panels - they will show "not generated yet" messages since data is null renderUserStats(); renderInfoBox(); renderThoughts(); updateDiceDisplayCore(); updateChatThoughts(); renderInventory(); renderEquipment(); renderQuests(); // console.log('[RPG Companion] Cache cleared successfully'); }); return settingsModal; } /** * Opens the dice rolling popup. * Backwards compatible wrapper for DiceModal class. */ export function openDicePopup() { if (diceModal) { diceModal.open(); } } /** * Closes the dice rolling popup. * Backwards compatible wrapper for DiceModal class. */ export function closeDicePopup() { if (diceModal) { diceModal.close(); } } /** * Opens the settings popup. * Backwards compatible wrapper for SettingsModal class. */ export function openSettingsPopup() { if (settingsModal) { settingsModal.open(); } } /** * Closes the settings popup. * Backwards compatible wrapper for SettingsModal class. */ export function closeSettingsPopup() { if (settingsModal) { settingsModal.close(); } } /** * @deprecated Legacy function - use diceModal._applyCustomTheme() instead */ export function applyCustomThemeToPopup() { if (diceModal) { diceModal._applyCustomTheme(); } } /** * Clears the last dice roll. * Backwards compatible wrapper for dice module. */ export function clearDiceRoll() { clearDiceRollCore(); } /** * Updates the dice display in the sidebar. * Backwards compatible wrapper for dice module. */ export function updateDiceDisplay() { updateDiceDisplayCore(); } /** * Adds the Roll Dice quick reply button. * Backwards compatible wrapper for dice module. */ export function addDiceQuickReply() { addDiceQuickReplyCore(); } /** * Returns the SettingsModal instance for external use * @returns {SettingsModal} The global SettingsModal instance */ export function getSettingsModal() { return settingsModal; } /** * Shows the welcome modal for v3.0.0 on first launch * Checks if user has already seen this version's welcome screen */ export function showWelcomeModalIfNeeded() { const WELCOME_VERSION = '3.0.1'; const STORAGE_KEY = 'rpg_companion_welcome_seen'; try { const seenVersion = localStorage.getItem(STORAGE_KEY); // If user hasn't seen v3.0.0 welcome yet, show it if (seenVersion !== WELCOME_VERSION) { showWelcomeModal(WELCOME_VERSION, STORAGE_KEY); } } catch (error) { console.error('[RPG Companion] Failed to check welcome modal status:', error); } } /** * Shows the deprecation notice once for users updating to the deprecation release. * @returns {boolean} True when the modal was displayed. */ export function showDeprecationModalIfNeeded() { const DEPRECATION_NOTICE_VERSION = '3.7.4'; const STORAGE_KEY = 'rpg_companion_deprecation_notice_seen'; try { const seenVersion = localStorage.getItem(STORAGE_KEY); if (seenVersion !== DEPRECATION_NOTICE_VERSION) { showDeprecationModal(DEPRECATION_NOTICE_VERSION, STORAGE_KEY); return true; } } catch (error) { console.error('[RPG Companion] Failed to check deprecation modal status:', error); } return false; } /** * Shows the welcome modal * @param {string} version - The version to mark as seen * @param {string} storageKey - The localStorage key to use */ function showWelcomeModal(version, storageKey) { const modal = document.getElementById('rpg-welcome-modal'); if (!modal) { console.error('[RPG Companion] Welcome modal element not found'); return; } // Apply current theme to modal const theme = extensionSettings.theme || 'default'; modal.setAttribute('data-theme', theme); // Show modal modal.style.display = 'flex'; modal.classList.add('is-open'); // Close button handler const closeBtn = document.getElementById('rpg-welcome-close'); const gotItBtn = document.getElementById('rpg-welcome-got-it'); const closeModal = () => { modal.classList.add('is-closing'); setTimeout(() => { modal.style.display = 'none'; modal.classList.remove('is-open', 'is-closing'); }, 200); // Mark this version as seen try { localStorage.setItem(storageKey, version); } catch (error) { console.error('[RPG Companion] Failed to save welcome modal status:', error); } }; // Attach event listeners closeBtn?.addEventListener('click', closeModal, { once: true }); gotItBtn?.addEventListener('click', closeModal, { once: true }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }, { once: true }); } function showDeprecationModal(version, storageKey) { const modal = document.getElementById('rpg-deprecation-modal'); if (!modal) { console.error('[RPG Companion] Deprecation modal element not found'); return; } const theme = extensionSettings.theme || 'default'; modal.setAttribute('data-theme', theme); modal.style.display = 'flex'; modal.classList.add('is-open'); const closeBtn = document.getElementById('rpg-deprecation-close'); const gotItBtn = document.getElementById('rpg-deprecation-got-it'); const closeModal = () => { modal.classList.add('is-closing'); setTimeout(() => { modal.style.display = 'none'; modal.classList.remove('is-open', 'is-closing'); }, 200); try { localStorage.setItem(storageKey, version); } catch (error) { console.error('[RPG Companion] Failed to save deprecation modal status:', error); } }; closeBtn?.addEventListener('click', closeModal, { once: true }); gotItBtn?.addEventListener('click', closeModal, { once: true }); modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }, { once: true }); }