refactor(ui): extract UI systems into modular architecture
Extracted ~920 lines of UI management code from index.js into 4 specialized modules to improve maintainability and organization. Modules Created: - src/systems/ui/theme.js (100 lines) - Theme management and custom colors - src/systems/ui/modals.js (568 lines) - DiceModal and SettingsModal ES6 classes - src/systems/ui/layout.js (254 lines) - Panel visibility, positioning, and collapse toggle - src/systems/ui/mobile.js (694 lines) - Mobile FAB, tabs, keyboard handling, and viewport management Changes: - Extracted theme application and custom color management - Extracted modal classes with proper state management - Extracted layout management (visibility, sections, positioning) - Extracted mobile-specific UI (FAB dragging with touch/mouse, tab navigation, keyboard handling) - Removed unused import (closeMobilePanelWithAnimation only used internally by mobile.js) - Updated imports in index.js to use new module structure - Added comprehensive documentation comments Result: - index.js reduced from 1606 to 921 lines (-685 lines) - All UI systems properly modularized with clean dependencies - Maintains 100% backward compatibility - All modules pass syntax validation Dependencies: - All modules import from src/core/state.js for shared state - Mobile module imports layout functions for panel animation - Layout module properly manages DOM element state
This commit is contained in:
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* Layout Management Module
|
||||||
|
* Handles panel visibility, section visibility, collapse/expand toggle, and panel positioning
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
$panelContainer,
|
||||||
|
$userStatsContainer,
|
||||||
|
$infoBoxContainer,
|
||||||
|
$thoughtsContainer
|
||||||
|
} from '../../core/state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of plot buttons based on settings.
|
||||||
|
*/
|
||||||
|
export function togglePlotButtons() {
|
||||||
|
if (extensionSettings.enablePlotButtons && extensionSettings.enabled) {
|
||||||
|
$('#rpg-plot-buttons').show();
|
||||||
|
} else {
|
||||||
|
$('#rpg-plot-buttons').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to close the mobile panel with animation.
|
||||||
|
*/
|
||||||
|
export function closeMobilePanelWithAnimation() {
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
|
|
||||||
|
// Add closing class to trigger slide-out animation
|
||||||
|
$panel.removeClass('rpg-mobile-open').addClass('rpg-mobile-closing');
|
||||||
|
$mobileToggle.removeClass('active');
|
||||||
|
|
||||||
|
// Wait for animation to complete before hiding
|
||||||
|
$panel.one('animationend', function() {
|
||||||
|
$panel.removeClass('rpg-mobile-closing');
|
||||||
|
$('.rpg-mobile-overlay').remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the collapse toggle icon direction based on panel position.
|
||||||
|
*/
|
||||||
|
export function updateCollapseToggleIcon() {
|
||||||
|
const $collapseToggle = $('#rpg-collapse-toggle');
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $icon = $collapseToggle.find('i');
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// Mobile: slides from right, use same icon logic as desktop right panel
|
||||||
|
const isOpen = $panel.hasClass('rpg-mobile-open');
|
||||||
|
console.log('[RPG Mobile] updateCollapseToggleIcon:', {
|
||||||
|
isMobile: true,
|
||||||
|
isOpen,
|
||||||
|
settingIcon: isOpen ? 'chevron-left' : 'chevron-right'
|
||||||
|
});
|
||||||
|
if (isOpen) {
|
||||||
|
// Panel open - chevron points left (to close/slide back right)
|
||||||
|
$icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-right').addClass('fa-chevron-left');
|
||||||
|
} else {
|
||||||
|
// Panel closed - chevron points right (to open/slide in from right)
|
||||||
|
$icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-left').addClass('fa-chevron-right');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Desktop: icon direction based on panel position and collapsed state
|
||||||
|
const isCollapsed = $panel.hasClass('rpg-collapsed');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
// When collapsed, arrow points inward (to expand)
|
||||||
|
if ($panel.hasClass('rpg-position-right')) {
|
||||||
|
$icon.removeClass('fa-chevron-right').addClass('fa-chevron-left');
|
||||||
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When expanded, arrow points outward (to collapse)
|
||||||
|
if ($panel.hasClass('rpg-position-right')) {
|
||||||
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
|
$icon.removeClass('fa-chevron-right').addClass('fa-chevron-left');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the collapse/expand toggle button for side panels.
|
||||||
|
*/
|
||||||
|
export function setupCollapseToggle() {
|
||||||
|
const $collapseToggle = $('#rpg-collapse-toggle');
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $icon = $collapseToggle.find('i');
|
||||||
|
|
||||||
|
$collapseToggle.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
|
||||||
|
// On mobile: button toggles panel open/closed (same as desktop behavior)
|
||||||
|
if (isMobile) {
|
||||||
|
const isOpen = $panel.hasClass('rpg-mobile-open');
|
||||||
|
console.log('[RPG Mobile] Collapse toggle clicked. Current state:', {
|
||||||
|
isOpen,
|
||||||
|
panelClasses: $panel.attr('class'),
|
||||||
|
inlineStyles: $panel.attr('style'),
|
||||||
|
panelPosition: {
|
||||||
|
top: $panel.css('top'),
|
||||||
|
bottom: $panel.css('bottom'),
|
||||||
|
transform: $panel.css('transform'),
|
||||||
|
visibility: $panel.css('visibility')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
// Close panel with animation
|
||||||
|
console.log('[RPG Mobile] Closing panel');
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
} else {
|
||||||
|
// Open panel
|
||||||
|
console.log('[RPG Mobile] Opening panel');
|
||||||
|
$panel.addClass('rpg-mobile-open');
|
||||||
|
const $overlay = $('<div class="rpg-mobile-overlay"></div>');
|
||||||
|
$('body').append($overlay);
|
||||||
|
|
||||||
|
// Debug: Check state after animation should complete
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[RPG Mobile] 500ms after opening:', {
|
||||||
|
panelClasses: $panel.attr('class'),
|
||||||
|
hasOpenClass: $panel.hasClass('rpg-mobile-open'),
|
||||||
|
visibility: $panel.css('visibility'),
|
||||||
|
transform: $panel.css('transform'),
|
||||||
|
display: $panel.css('display'),
|
||||||
|
opacity: $panel.css('opacity')
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Close when clicking overlay
|
||||||
|
$overlay.on('click', function() {
|
||||||
|
console.log('[RPG Mobile] Overlay clicked - closing panel');
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update icon to reflect new state
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] After toggle:', {
|
||||||
|
panelClasses: $panel.attr('class'),
|
||||||
|
inlineStyles: $panel.attr('style'),
|
||||||
|
panelPosition: {
|
||||||
|
top: $panel.css('top'),
|
||||||
|
bottom: $panel.css('bottom'),
|
||||||
|
transform: $panel.css('transform'),
|
||||||
|
visibility: $panel.css('visibility')
|
||||||
|
},
|
||||||
|
gameContainer: {
|
||||||
|
opacity: $('.rpg-game-container').css('opacity'),
|
||||||
|
visibility: $('.rpg-game-container').css('visibility')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop behavior: collapse/expand side panel
|
||||||
|
const isCollapsed = $panel.hasClass('rpg-collapsed');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
// Expand panel
|
||||||
|
$panel.removeClass('rpg-collapsed');
|
||||||
|
|
||||||
|
// Update icon based on position
|
||||||
|
if ($panel.hasClass('rpg-position-right')) {
|
||||||
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
|
$icon.removeClass('fa-chevron-right').addClass('fa-chevron-left');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Collapse panel
|
||||||
|
$panel.addClass('rpg-collapsed');
|
||||||
|
|
||||||
|
// Update icon based on position
|
||||||
|
if ($panel.hasClass('rpg-position-right')) {
|
||||||
|
$icon.removeClass('fa-chevron-right').addClass('fa-chevron-left');
|
||||||
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial icon direction based on panel position
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of the entire panel.
|
||||||
|
*/
|
||||||
|
export function updatePanelVisibility() {
|
||||||
|
if (extensionSettings.enabled) {
|
||||||
|
$panelContainer.show();
|
||||||
|
togglePlotButtons(); // Update plot button visibility
|
||||||
|
} else {
|
||||||
|
$panelContainer.hide();
|
||||||
|
$('#rpg-plot-buttons').hide(); // Hide plot buttons when disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of individual sections.
|
||||||
|
*/
|
||||||
|
export function updateSectionVisibility() {
|
||||||
|
// Show/hide sections based on settings
|
||||||
|
$userStatsContainer.toggle(extensionSettings.showUserStats);
|
||||||
|
$infoBoxContainer.toggle(extensionSettings.showInfoBox);
|
||||||
|
$thoughtsContainer.toggle(extensionSettings.showCharacterThoughts);
|
||||||
|
|
||||||
|
// Show/hide dividers intelligently
|
||||||
|
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
|
||||||
|
const showDividerAfterStats = extensionSettings.showUserStats &&
|
||||||
|
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts);
|
||||||
|
$('#rpg-divider-stats').toggle(showDividerAfterStats);
|
||||||
|
|
||||||
|
// Divider after Info Box: shown if Info Box is visible AND Mind Reading is visible
|
||||||
|
const showDividerAfterInfo = extensionSettings.showInfoBox &&
|
||||||
|
extensionSettings.showCharacterThoughts;
|
||||||
|
$('#rpg-divider-info').toggle(showDividerAfterInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the selected panel position.
|
||||||
|
*/
|
||||||
|
export function applyPanelPosition() {
|
||||||
|
if (!$panelContainer) return;
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
|
||||||
|
// Remove all position classes
|
||||||
|
$panelContainer.removeClass('rpg-position-left rpg-position-right rpg-position-top');
|
||||||
|
|
||||||
|
// On mobile, don't apply desktop position classes
|
||||||
|
if (isMobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop: Add the appropriate position class
|
||||||
|
$panelContainer.addClass(`rpg-position-${extensionSettings.panelPosition}`);
|
||||||
|
|
||||||
|
// Update collapse toggle icon direction for new position
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
}
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
/**
|
||||||
|
* Mobile UI Module
|
||||||
|
* Handles mobile-specific UI functionality: FAB dragging, tabs, keyboard handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings } from '../../core/state.js';
|
||||||
|
import { saveSettings } from '../../core/persistence.js';
|
||||||
|
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the mobile toggle button (FAB) with drag functionality.
|
||||||
|
* Handles touch/mouse events for positioning and panel toggling.
|
||||||
|
*/
|
||||||
|
export function setupMobileToggle() {
|
||||||
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $overlay = $('<div class="rpg-mobile-overlay"></div>');
|
||||||
|
|
||||||
|
// DIAGNOSTIC: Check if elements exist and log setup state
|
||||||
|
console.log('[RPG Mobile] ========================================');
|
||||||
|
console.log('[RPG Mobile] setupMobileToggle called');
|
||||||
|
console.log('[RPG Mobile] Button exists:', $mobileToggle.length > 0, 'jQuery object:', $mobileToggle);
|
||||||
|
console.log('[RPG Mobile] Panel exists:', $panel.length > 0);
|
||||||
|
console.log('[RPG Mobile] Window width:', window.innerWidth);
|
||||||
|
console.log('[RPG Mobile] Is mobile viewport (<=1000):', window.innerWidth <= 1000);
|
||||||
|
console.log('[RPG Mobile] ========================================');
|
||||||
|
|
||||||
|
if ($mobileToggle.length === 0) {
|
||||||
|
console.error('[RPG Mobile] ERROR: Mobile toggle button not found in DOM!');
|
||||||
|
console.error('[RPG Mobile] Cannot attach event handlers - button does not exist');
|
||||||
|
return; // Exit early if button doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and apply saved FAB position
|
||||||
|
if (extensionSettings.mobileFabPosition) {
|
||||||
|
const pos = extensionSettings.mobileFabPosition;
|
||||||
|
console.log('[RPG Mobile] Loading saved FAB position:', pos);
|
||||||
|
|
||||||
|
// Apply saved position
|
||||||
|
if (pos.top) $mobileToggle.css('top', pos.top);
|
||||||
|
if (pos.right) $mobileToggle.css('right', pos.right);
|
||||||
|
if (pos.bottom) $mobileToggle.css('bottom', pos.bottom);
|
||||||
|
if (pos.left) $mobileToggle.css('left', pos.left);
|
||||||
|
|
||||||
|
// Constrain to viewport after position is applied
|
||||||
|
requestAnimationFrame(() => constrainFabToViewport());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch/drag state
|
||||||
|
let isDragging = false;
|
||||||
|
let touchStartTime = 0;
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
let buttonStartX = 0;
|
||||||
|
let buttonStartY = 0;
|
||||||
|
const LONG_PRESS_DURATION = 200; // ms to hold before enabling drag
|
||||||
|
const MOVE_THRESHOLD = 10; // px to move before enabling drag
|
||||||
|
let rafId = null; // RequestAnimationFrame ID for smooth updates
|
||||||
|
let pendingX = null;
|
||||||
|
let pendingY = null;
|
||||||
|
|
||||||
|
// Update position using requestAnimationFrame for smooth rendering
|
||||||
|
function updateFabPosition() {
|
||||||
|
if (pendingX !== null && pendingY !== null) {
|
||||||
|
$mobileToggle.css({
|
||||||
|
left: pendingX + 'px',
|
||||||
|
top: pendingY + 'px',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto'
|
||||||
|
});
|
||||||
|
pendingX = null;
|
||||||
|
pendingY = null;
|
||||||
|
}
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch start - begin tracking
|
||||||
|
$mobileToggle.on('touchstart', function(e) {
|
||||||
|
const touch = e.originalEvent.touches[0];
|
||||||
|
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
touchStartX = touch.clientX;
|
||||||
|
touchStartY = touch.clientY;
|
||||||
|
|
||||||
|
const offset = $mobileToggle.offset();
|
||||||
|
buttonStartX = offset.left;
|
||||||
|
buttonStartY = offset.top;
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch move - check if should start dragging
|
||||||
|
$mobileToggle.on('touchmove', function(e) {
|
||||||
|
const touch = e.originalEvent.touches[0];
|
||||||
|
const deltaX = touch.clientX - touchStartX;
|
||||||
|
const deltaY = touch.clientY - touchStartY;
|
||||||
|
const timeSinceStart = Date.now() - touchStartTime;
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
|
||||||
|
// Start dragging if held long enough OR moved far enough
|
||||||
|
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
||||||
|
isDragging = true;
|
||||||
|
$mobileToggle.addClass('dragging'); // Disable transitions while dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
e.preventDefault(); // Prevent scrolling while dragging
|
||||||
|
|
||||||
|
// Calculate new position
|
||||||
|
let newX = buttonStartX + deltaX;
|
||||||
|
let newY = buttonStartY + deltaY;
|
||||||
|
|
||||||
|
// Get button dimensions
|
||||||
|
const buttonWidth = $mobileToggle.outerWidth();
|
||||||
|
const buttonHeight = $mobileToggle.outerHeight();
|
||||||
|
|
||||||
|
// Constrain to viewport with 10px padding
|
||||||
|
const minX = 10;
|
||||||
|
const maxX = window.innerWidth - buttonWidth - 10;
|
||||||
|
const minY = 10;
|
||||||
|
const maxY = window.innerHeight - buttonHeight - 10;
|
||||||
|
|
||||||
|
newX = Math.max(minX, Math.min(maxX, newX));
|
||||||
|
newY = Math.max(minY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
// Store pending position and request animation frame for smooth update
|
||||||
|
pendingX = newX;
|
||||||
|
pendingY = newY;
|
||||||
|
if (!rafId) {
|
||||||
|
rafId = requestAnimationFrame(updateFabPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse drag support for desktop
|
||||||
|
let mouseDown = false;
|
||||||
|
|
||||||
|
$mobileToggle.on('mousedown', function(e) {
|
||||||
|
// Prevent default to avoid text selection
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
touchStartX = e.clientX;
|
||||||
|
touchStartY = e.clientY;
|
||||||
|
|
||||||
|
const offset = $mobileToggle.offset();
|
||||||
|
buttonStartX = offset.left;
|
||||||
|
buttonStartY = offset.top;
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
mouseDown = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse move - only track if mouse is down
|
||||||
|
$(document).on('mousemove', function(e) {
|
||||||
|
if (!mouseDown) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - touchStartX;
|
||||||
|
const deltaY = e.clientY - touchStartY;
|
||||||
|
const timeSinceStart = Date.now() - touchStartTime;
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
|
||||||
|
// Start dragging if held long enough OR moved far enough
|
||||||
|
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
||||||
|
isDragging = true;
|
||||||
|
$mobileToggle.addClass('dragging'); // Disable transitions while dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Calculate new position
|
||||||
|
let newX = buttonStartX + deltaX;
|
||||||
|
let newY = buttonStartY + deltaY;
|
||||||
|
|
||||||
|
// Get button dimensions
|
||||||
|
const buttonWidth = $mobileToggle.outerWidth();
|
||||||
|
const buttonHeight = $mobileToggle.outerHeight();
|
||||||
|
|
||||||
|
// Constrain to viewport with 10px padding
|
||||||
|
const minX = 10;
|
||||||
|
const maxX = window.innerWidth - buttonWidth - 10;
|
||||||
|
const minY = 10;
|
||||||
|
const maxY = window.innerHeight - buttonHeight - 10;
|
||||||
|
|
||||||
|
newX = Math.max(minX, Math.min(maxX, newX));
|
||||||
|
newY = Math.max(minY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
// Store pending position and request animation frame for smooth update
|
||||||
|
pendingX = newX;
|
||||||
|
pendingY = newY;
|
||||||
|
if (!rafId) {
|
||||||
|
rafId = requestAnimationFrame(updateFabPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse up - save position or let click handler toggle
|
||||||
|
$(document).on('mouseup', function(e) {
|
||||||
|
if (!mouseDown) return;
|
||||||
|
|
||||||
|
mouseDown = false;
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
// Was dragging - save new position
|
||||||
|
const offset = $mobileToggle.offset();
|
||||||
|
const newPosition = {
|
||||||
|
left: offset.left + 'px',
|
||||||
|
top: offset.top + 'px'
|
||||||
|
};
|
||||||
|
|
||||||
|
extensionSettings.mobileFabPosition = newPosition;
|
||||||
|
saveSettings();
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition);
|
||||||
|
|
||||||
|
// Constrain to viewport bounds (now that position is saved)
|
||||||
|
setTimeout(() => constrainFabToViewport(), 10);
|
||||||
|
|
||||||
|
// Re-enable transitions with smooth animation
|
||||||
|
setTimeout(() => {
|
||||||
|
$mobileToggle.removeClass('dragging');
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
// Prevent click from firing after drag
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Add flag to prevent click handler from firing
|
||||||
|
$mobileToggle.data('just-dragged', true);
|
||||||
|
setTimeout(() => {
|
||||||
|
$mobileToggle.data('just-dragged', false);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
// If not dragging, let the click handler toggle the panel
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch end - save position or toggle panel
|
||||||
|
$mobileToggle.on('touchend', function(e) {
|
||||||
|
// TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback
|
||||||
|
// e.preventDefault();
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
// Was dragging - save new position
|
||||||
|
const offset = $mobileToggle.offset();
|
||||||
|
const newPosition = {
|
||||||
|
left: offset.left + 'px',
|
||||||
|
top: offset.top + 'px'
|
||||||
|
};
|
||||||
|
|
||||||
|
extensionSettings.mobileFabPosition = newPosition;
|
||||||
|
saveSettings();
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
||||||
|
|
||||||
|
// Constrain to viewport bounds (now that position is saved)
|
||||||
|
setTimeout(() => constrainFabToViewport(), 10);
|
||||||
|
|
||||||
|
// Re-enable transitions with smooth animation
|
||||||
|
setTimeout(() => {
|
||||||
|
$mobileToggle.removeClass('dragging');
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
} else {
|
||||||
|
// Was a tap - toggle panel
|
||||||
|
console.log('[RPG Mobile] Quick tap detected - toggling panel');
|
||||||
|
|
||||||
|
if ($panel.hasClass('rpg-mobile-open')) {
|
||||||
|
// Close panel with animation
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
} else {
|
||||||
|
// Open panel
|
||||||
|
$panel.addClass('rpg-mobile-open');
|
||||||
|
$('body').append($overlay);
|
||||||
|
$mobileToggle.addClass('active');
|
||||||
|
|
||||||
|
// Close when clicking overlay
|
||||||
|
$overlay.on('click', function() {
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click handler - works on both mobile and desktop
|
||||||
|
$mobileToggle.on('click', function(e) {
|
||||||
|
// Skip if we just finished dragging
|
||||||
|
if ($mobileToggle.data('just-dragged')) {
|
||||||
|
console.log('[RPG Mobile] Click blocked - just finished dragging');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] >>> CLICK EVENT FIRED <<<', {
|
||||||
|
windowWidth: window.innerWidth,
|
||||||
|
isMobileViewport: window.innerWidth <= 1000,
|
||||||
|
panelOpen: $panel.hasClass('rpg-mobile-open')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Work on both mobile and desktop (removed viewport check)
|
||||||
|
if ($panel.hasClass('rpg-mobile-open')) {
|
||||||
|
console.log('[RPG Mobile] Click: Closing panel');
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
} else {
|
||||||
|
console.log('[RPG Mobile] Click: Opening panel');
|
||||||
|
$panel.addClass('rpg-mobile-open');
|
||||||
|
$('body').append($overlay);
|
||||||
|
$mobileToggle.addClass('active');
|
||||||
|
|
||||||
|
$overlay.on('click', function() {
|
||||||
|
console.log('[RPG Mobile] Overlay clicked - closing panel');
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle viewport resize to manage desktop/mobile transitions
|
||||||
|
let wasMobile = window.innerWidth <= 1000;
|
||||||
|
let resizeTimer;
|
||||||
|
|
||||||
|
$(window).on('resize', function() {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
|
|
||||||
|
// Transitioning from desktop to mobile - handle immediately for smooth transition
|
||||||
|
if (!wasMobile && isMobile) {
|
||||||
|
console.log('[RPG Mobile] Transitioning desktop -> mobile');
|
||||||
|
|
||||||
|
// Remove desktop positioning classes
|
||||||
|
$panel.removeClass('rpg-position-right rpg-position-left rpg-position-top');
|
||||||
|
|
||||||
|
// Clear collapsed state - mobile doesn't use collapse
|
||||||
|
$panel.removeClass('rpg-collapsed');
|
||||||
|
|
||||||
|
// Close panel on mobile with animation
|
||||||
|
closeMobilePanelWithAnimation();
|
||||||
|
|
||||||
|
// Clear any inline styles that might be overriding CSS
|
||||||
|
$panel.attr('style', '');
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] After cleanup:', {
|
||||||
|
panelClasses: $panel.attr('class'),
|
||||||
|
inlineStyles: $panel.attr('style'),
|
||||||
|
panelPosition: {
|
||||||
|
top: $panel.css('top'),
|
||||||
|
bottom: $panel.css('bottom'),
|
||||||
|
transform: $panel.css('transform'),
|
||||||
|
visibility: $panel.css('visibility')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up mobile tabs IMMEDIATELY (no debounce delay)
|
||||||
|
setupMobileTabs();
|
||||||
|
|
||||||
|
// Update icon for mobile state
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
|
||||||
|
wasMobile = isMobile;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For mobile to desktop transition, use debounce
|
||||||
|
resizeTimer = setTimeout(function() {
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
|
||||||
|
// Transitioning from mobile to desktop
|
||||||
|
if (wasMobile && !isMobile) {
|
||||||
|
// Disable transitions to prevent left→right slide animation
|
||||||
|
$panel.css('transition', 'none');
|
||||||
|
|
||||||
|
$panel.removeClass('rpg-mobile-open rpg-mobile-closing');
|
||||||
|
$mobileToggle.removeClass('active');
|
||||||
|
$('.rpg-mobile-overlay').remove();
|
||||||
|
|
||||||
|
// Restore desktop positioning class
|
||||||
|
const position = extensionSettings.panelPosition || 'right';
|
||||||
|
$panel.addClass('rpg-position-' + position);
|
||||||
|
|
||||||
|
// Remove mobile tabs structure
|
||||||
|
removeMobileTabs();
|
||||||
|
|
||||||
|
// Force reflow to apply position instantly
|
||||||
|
$panel[0].offsetHeight;
|
||||||
|
|
||||||
|
// Re-enable transitions after positioned
|
||||||
|
setTimeout(function() {
|
||||||
|
$panel.css('transition', '');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasMobile = isMobile;
|
||||||
|
|
||||||
|
// Constrain FAB to viewport after resize (only if user has positioned it)
|
||||||
|
constrainFabToViewport();
|
||||||
|
}, 150); // Debounce only for mobile→desktop
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize mobile tabs if starting on mobile
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
if (isMobile) {
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
// Clear any inline styles
|
||||||
|
$panel.attr('style', '');
|
||||||
|
|
||||||
|
console.log('[RPG Mobile] Initial load on mobile viewport:', {
|
||||||
|
panelClasses: $panel.attr('class'),
|
||||||
|
inlineStyles: $panel.attr('style'),
|
||||||
|
panelPosition: {
|
||||||
|
top: $panel.css('top'),
|
||||||
|
bottom: $panel.css('top'),
|
||||||
|
transform: $panel.css('transform'),
|
||||||
|
visibility: $panel.css('visibility')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setupMobileTabs();
|
||||||
|
// Set initial icon for mobile
|
||||||
|
updateCollapseToggleIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrains the mobile FAB button to viewport bounds with top-bar awareness.
|
||||||
|
* Only runs when button is in user-controlled state (mobileFabPosition exists).
|
||||||
|
* Ensures button never goes behind the top bar or outside viewport edges.
|
||||||
|
*/
|
||||||
|
export function constrainFabToViewport() {
|
||||||
|
// Only constrain if user has set a custom position
|
||||||
|
if (!extensionSettings.mobileFabPosition) {
|
||||||
|
console.log('[RPG Mobile] Skipping viewport constraint - using CSS defaults');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
|
if ($mobileToggle.length === 0) return;
|
||||||
|
|
||||||
|
// Skip if button is not visible
|
||||||
|
if (!$mobileToggle.is(':visible')) {
|
||||||
|
console.log('[RPG Mobile] Skipping viewport constraint - button not visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current position
|
||||||
|
const offset = $mobileToggle.offset();
|
||||||
|
if (!offset) return;
|
||||||
|
|
||||||
|
let currentX = offset.left;
|
||||||
|
let currentY = offset.top;
|
||||||
|
|
||||||
|
const buttonWidth = $mobileToggle.outerWidth();
|
||||||
|
const buttonHeight = $mobileToggle.outerHeight();
|
||||||
|
|
||||||
|
// Get top bar height from CSS variable (fallback to 50px if not set)
|
||||||
|
const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--topBarBlockSize')) || 50;
|
||||||
|
|
||||||
|
// Calculate viewport bounds with padding
|
||||||
|
// Use top bar height + extra padding for top bound
|
||||||
|
const minX = 10;
|
||||||
|
const maxX = window.innerWidth - buttonWidth - 10;
|
||||||
|
const minY = topBarHeight + 60; // Top bar + extra space for visibility
|
||||||
|
const maxY = window.innerHeight - buttonHeight - 10;
|
||||||
|
|
||||||
|
// Constrain to bounds
|
||||||
|
let newX = Math.max(minX, Math.min(maxX, currentX));
|
||||||
|
let newY = Math.max(minY, Math.min(maxY, currentY));
|
||||||
|
|
||||||
|
// Only update if position changed
|
||||||
|
if (newX !== currentX || newY !== currentY) {
|
||||||
|
console.log('[RPG Mobile] Constraining FAB to viewport:', {
|
||||||
|
old: { x: currentX, y: currentY },
|
||||||
|
new: { x: newX, y: newY },
|
||||||
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||||
|
topBarHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply new position
|
||||||
|
$mobileToggle.css({
|
||||||
|
left: newX + 'px',
|
||||||
|
top: newY + 'px',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save corrected position
|
||||||
|
extensionSettings.mobileFabPosition = {
|
||||||
|
left: newX + 'px',
|
||||||
|
top: newY + 'px'
|
||||||
|
};
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up mobile tab navigation for organizing content.
|
||||||
|
* Only runs on mobile viewports (<=1000px).
|
||||||
|
*/
|
||||||
|
export function setupMobileTabs() {
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
if (!isMobile) return;
|
||||||
|
|
||||||
|
// Check if tabs already exist
|
||||||
|
if ($('.rpg-mobile-tabs').length > 0) return;
|
||||||
|
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $contentBox = $panel.find('.rpg-content-box');
|
||||||
|
|
||||||
|
// Get existing sections
|
||||||
|
const $userStats = $('#rpg-user-stats');
|
||||||
|
const $infoBox = $('#rpg-info-box');
|
||||||
|
const $thoughts = $('#rpg-thoughts');
|
||||||
|
|
||||||
|
// If no sections exist, nothing to organize
|
||||||
|
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tab navigation (only show tabs for sections that exist)
|
||||||
|
const tabs = [];
|
||||||
|
const hasInfoOrCharacters = $infoBox.length > 0 || $thoughts.length > 0;
|
||||||
|
|
||||||
|
if ($userStats.length > 0) {
|
||||||
|
tabs.push('<button class="rpg-mobile-tab active" data-tab="stats"><i class="fa-solid fa-chart-bar"></i><span>Stats</span></button>');
|
||||||
|
}
|
||||||
|
// Combine Info and Characters into one tab
|
||||||
|
if (hasInfoOrCharacters) {
|
||||||
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="info-characters"><i class="fa-solid fa-book"></i><span>Info</span></button>');
|
||||||
|
}
|
||||||
|
|
||||||
|
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
||||||
|
|
||||||
|
// Determine which tab should be active
|
||||||
|
let firstTab = '';
|
||||||
|
if ($userStats.length > 0) firstTab = 'stats';
|
||||||
|
else if (hasInfoOrCharacters) firstTab = 'info-characters';
|
||||||
|
|
||||||
|
// Create tab content wrappers
|
||||||
|
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
||||||
|
const $infoCharactersTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info-characters' ? 'active' : '') + '" data-tab-content="info-characters"></div>');
|
||||||
|
|
||||||
|
// Create combined content wrapper for Info and Characters
|
||||||
|
const $combinedWrapper = $('<div class="rpg-mobile-combined-content"></div>');
|
||||||
|
|
||||||
|
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||||
|
if ($userStats.length > 0) {
|
||||||
|
$statsTab.append($userStats.detach());
|
||||||
|
$userStats.show();
|
||||||
|
}
|
||||||
|
if ($infoBox.length > 0) {
|
||||||
|
$combinedWrapper.append($infoBox.detach());
|
||||||
|
$infoBox.show();
|
||||||
|
}
|
||||||
|
if ($thoughts.length > 0) {
|
||||||
|
$combinedWrapper.append($thoughts.detach());
|
||||||
|
$thoughts.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add combined wrapper to the info-characters tab
|
||||||
|
if (hasInfoOrCharacters) {
|
||||||
|
$infoCharactersTab.append($combinedWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide dividers on mobile
|
||||||
|
$('.rpg-divider').hide();
|
||||||
|
|
||||||
|
// Build mobile tab structure
|
||||||
|
const $mobileContainer = $('<div class="rpg-mobile-container"></div>');
|
||||||
|
$mobileContainer.append($tabNav);
|
||||||
|
|
||||||
|
// Only append tab content wrappers that have content
|
||||||
|
if ($userStats.length > 0) $mobileContainer.append($statsTab);
|
||||||
|
if (hasInfoOrCharacters) $mobileContainer.append($infoCharactersTab);
|
||||||
|
|
||||||
|
// Insert mobile tab structure at the beginning of content box
|
||||||
|
$contentBox.prepend($mobileContainer);
|
||||||
|
|
||||||
|
// Handle tab switching
|
||||||
|
$tabNav.find('.rpg-mobile-tab').on('click', function() {
|
||||||
|
const tabName = $(this).data('tab');
|
||||||
|
|
||||||
|
// Update active tab button
|
||||||
|
$tabNav.find('.rpg-mobile-tab').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
|
||||||
|
// Update active tab content
|
||||||
|
$mobileContainer.find('.rpg-mobile-tab-content').removeClass('active');
|
||||||
|
$mobileContainer.find('[data-tab-content="' + tabName + '"]').addClass('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes mobile tab navigation and restores desktop layout.
|
||||||
|
*/
|
||||||
|
export function removeMobileTabs() {
|
||||||
|
// Get sections from tabs before removing
|
||||||
|
const $userStats = $('#rpg-user-stats').detach();
|
||||||
|
const $infoBox = $('#rpg-info-box').detach();
|
||||||
|
const $thoughts = $('#rpg-thoughts').detach();
|
||||||
|
|
||||||
|
// Remove mobile tab container
|
||||||
|
$('.rpg-mobile-container').remove();
|
||||||
|
|
||||||
|
// Get dividers
|
||||||
|
const $dividerStats = $('#rpg-divider-stats');
|
||||||
|
const $dividerInfo = $('#rpg-divider-info');
|
||||||
|
|
||||||
|
// Restore original sections to content box in correct order
|
||||||
|
const $contentBox = $('.rpg-content-box');
|
||||||
|
|
||||||
|
// Re-insert sections in original order
|
||||||
|
if ($dividerStats.length) {
|
||||||
|
$dividerStats.before($userStats);
|
||||||
|
$dividerInfo.before($infoBox);
|
||||||
|
$contentBox.append($thoughts);
|
||||||
|
} else {
|
||||||
|
// Fallback if dividers don't exist
|
||||||
|
$contentBox.prepend($thoughts);
|
||||||
|
$contentBox.prepend($infoBox);
|
||||||
|
$contentBox.prepend($userStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show sections and dividers
|
||||||
|
$userStats.show();
|
||||||
|
$infoBox.show();
|
||||||
|
$thoughts.show();
|
||||||
|
$('.rpg-divider').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up mobile keyboard handling using Visual Viewport API.
|
||||||
|
* Prevents layout squashing when keyboard appears by detecting
|
||||||
|
* viewport changes and adding CSS classes for adjustment.
|
||||||
|
*/
|
||||||
|
export function setupMobileKeyboardHandling() {
|
||||||
|
if (!window.visualViewport) {
|
||||||
|
// console.log('[RPG Mobile] Visual Viewport API not supported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
let keyboardVisible = false;
|
||||||
|
|
||||||
|
// Listen for viewport resize (keyboard show/hide)
|
||||||
|
window.visualViewport.addEventListener('resize', () => {
|
||||||
|
// Only handle if panel is open on mobile
|
||||||
|
if (!$panel.hasClass('rpg-mobile-open')) return;
|
||||||
|
|
||||||
|
const viewportHeight = window.visualViewport.height;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Keyboard visible if viewport significantly smaller than window
|
||||||
|
// Using 75% threshold to account for browser UI variations
|
||||||
|
const isKeyboardShowing = viewportHeight < windowHeight * 0.75;
|
||||||
|
|
||||||
|
if (isKeyboardShowing && !keyboardVisible) {
|
||||||
|
// Keyboard just appeared
|
||||||
|
keyboardVisible = true;
|
||||||
|
$panel.addClass('rpg-keyboard-visible');
|
||||||
|
// console.log('[RPG Mobile] Keyboard opened');
|
||||||
|
} else if (!isKeyboardShowing && keyboardVisible) {
|
||||||
|
// Keyboard just disappeared
|
||||||
|
keyboardVisible = false;
|
||||||
|
$panel.removeClass('rpg-keyboard-visible');
|
||||||
|
// console.log('[RPG Mobile] Keyboard closed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
|
||||||
|
* Uses smooth scrolling to bring focused field into view with proper padding.
|
||||||
|
*/
|
||||||
|
export function setupContentEditableScrolling() {
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
|
||||||
|
// Use event delegation for all contenteditable fields
|
||||||
|
$panel.on('focusin', '[contenteditable="true"]', function(e) {
|
||||||
|
const $field = $(this);
|
||||||
|
|
||||||
|
// Small delay to let keyboard animate in
|
||||||
|
setTimeout(() => {
|
||||||
|
// Scroll field into view with padding
|
||||||
|
// Using 'center' to ensure field is in middle of viewport
|
||||||
|
$field[0].scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
/**
|
||||||
|
* Modal Management Module
|
||||||
|
* Handles DiceModal and SettingsModal ES6 classes with state management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
lastGeneratedData,
|
||||||
|
committedTrackerData,
|
||||||
|
pendingDiceRoll,
|
||||||
|
$infoBoxContainer,
|
||||||
|
$thoughtsContainer,
|
||||||
|
setPendingDiceRoll
|
||||||
|
} from '../../core/state.js';
|
||||||
|
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||||
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
|
import { updateChatThoughts } from '../rendering/thoughts.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<number>} 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 rollDice();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save roll button (closes popup and saves the roll)
|
||||||
|
$('#rpg-dice-save-btn').on('click', function() {
|
||||||
|
// Save the pending roll
|
||||||
|
if (pendingDiceRoll) {
|
||||||
|
extensionSettings.lastDiceRoll = pendingDiceRoll;
|
||||||
|
saveSettings();
|
||||||
|
updateDiceDisplay();
|
||||||
|
setPendingDiceRoll(null);
|
||||||
|
}
|
||||||
|
closeDicePopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset on Enter key
|
||||||
|
$('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
rollDice();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear dice roll button
|
||||||
|
$('#rpg-clear-dice').on('click', function(e) {
|
||||||
|
e.stopPropagation(); // Prevent opening the dice popup
|
||||||
|
clearDiceRoll();
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// Clear the data
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
|
||||||
|
// Clear committed tracker data (used for generation context)
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
|
||||||
|
// Clear all message swipe data
|
||||||
|
const chat = getContext().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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the UI
|
||||||
|
if ($infoBoxContainer) {
|
||||||
|
$infoBoxContainer.empty();
|
||||||
|
}
|
||||||
|
if ($thoughtsContainer) {
|
||||||
|
$thoughtsContainer.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset stats to defaults and re-render
|
||||||
|
extensionSettings.userStats = {
|
||||||
|
health: 100,
|
||||||
|
satiety: 100,
|
||||||
|
energy: 100,
|
||||||
|
hygiene: 100,
|
||||||
|
arousal: 0,
|
||||||
|
mood: '😐',
|
||||||
|
conditions: 'None',
|
||||||
|
inventory: 'None'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Save everything
|
||||||
|
saveChatData();
|
||||||
|
saveSettings();
|
||||||
|
|
||||||
|
// Re-render user stats and dice display
|
||||||
|
renderUserStats();
|
||||||
|
updateDiceDisplay();
|
||||||
|
updateChatThoughts(); // Clear the thought bubble in chat
|
||||||
|
|
||||||
|
// console.log('[RPG Companion] Chat cache cleared');
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export function clearDiceRoll() {
|
||||||
|
extensionSettings.lastDiceRoll = null;
|
||||||
|
saveSettings();
|
||||||
|
updateDiceDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rolls the dice and displays result.
|
||||||
|
* Refactored to use DiceModal class.
|
||||||
|
*/
|
||||||
|
async function rollDice() {
|
||||||
|
if (!diceModal) return;
|
||||||
|
|
||||||
|
const count = parseInt(String($('#rpg-dice-count').val())) || 1;
|
||||||
|
const sides = parseInt(String($('#rpg-dice-sides').val())) || 20;
|
||||||
|
|
||||||
|
// Start rolling animation
|
||||||
|
diceModal.startRolling();
|
||||||
|
|
||||||
|
// Wait for animation (simulate rolling)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200));
|
||||||
|
|
||||||
|
// Execute /roll command
|
||||||
|
const rollCommand = `/roll ${count}d${sides}`;
|
||||||
|
const rollResult = await executeRollCommand(rollCommand);
|
||||||
|
|
||||||
|
// Parse result
|
||||||
|
const total = rollResult.total || 0;
|
||||||
|
const rolls = rollResult.rolls || [];
|
||||||
|
|
||||||
|
// Store result temporarily (not saved until "Save Roll" is clicked)
|
||||||
|
setPendingDiceRoll({
|
||||||
|
formula: `${count}d${sides}`,
|
||||||
|
total: total,
|
||||||
|
rolls: rolls,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show result
|
||||||
|
diceModal.showResult(total, rolls);
|
||||||
|
|
||||||
|
// Don't update sidebar display yet - only update when user clicks "Save Roll"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a /roll command and returns the result.
|
||||||
|
*/
|
||||||
|
async function executeRollCommand(command) {
|
||||||
|
try {
|
||||||
|
// Parse the dice notation (e.g., "2d20")
|
||||||
|
const match = command.match(/(\d+)d(\d+)/);
|
||||||
|
if (!match) {
|
||||||
|
return { total: 0, rolls: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = parseInt(match[1]);
|
||||||
|
const sides = parseInt(match[2]);
|
||||||
|
const rolls = [];
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const roll = Math.floor(Math.random() * sides) + 1;
|
||||||
|
rolls.push(roll);
|
||||||
|
total += roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, rolls };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RPG Companion] Error rolling dice:', error);
|
||||||
|
return { total: 0, rolls: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the dice display in the sidebar.
|
||||||
|
*/
|
||||||
|
export function updateDiceDisplay() {
|
||||||
|
const lastRoll = extensionSettings.lastDiceRoll;
|
||||||
|
if (lastRoll) {
|
||||||
|
$('#rpg-last-roll-text').text(`Last Roll (${lastRoll.formula}): ${lastRoll.total}`);
|
||||||
|
} else {
|
||||||
|
$('#rpg-last-roll-text').text('Last Roll: None');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the Roll Dice quick reply button.
|
||||||
|
*/
|
||||||
|
export function addDiceQuickReply() {
|
||||||
|
// Create quick reply button if Quick Replies exist
|
||||||
|
if (window.quickReplyApi) {
|
||||||
|
// Quick Reply API integration would go here
|
||||||
|
// For now, the dice display in the sidebar serves as the button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SettingsModal instance for external use
|
||||||
|
* @returns {SettingsModal} The global SettingsModal instance
|
||||||
|
*/
|
||||||
|
export function getSettingsModal() {
|
||||||
|
return settingsModal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Theme Management Module
|
||||||
|
* Handles theme application, custom colors, and animations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the selected theme to the panel.
|
||||||
|
*/
|
||||||
|
export function applyTheme() {
|
||||||
|
if (!$panelContainer) return;
|
||||||
|
|
||||||
|
const theme = extensionSettings.theme;
|
||||||
|
|
||||||
|
// Remove all theme attributes first
|
||||||
|
$panelContainer.removeAttr('data-theme');
|
||||||
|
|
||||||
|
// Clear any inline CSS variable overrides
|
||||||
|
$panelContainer.css({
|
||||||
|
'--rpg-bg': '',
|
||||||
|
'--rpg-accent': '',
|
||||||
|
'--rpg-text': '',
|
||||||
|
'--rpg-highlight': '',
|
||||||
|
'--rpg-border': '',
|
||||||
|
'--rpg-shadow': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the selected theme
|
||||||
|
if (theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
} else if (theme !== 'default') {
|
||||||
|
// For non-default themes, set the data-theme attribute
|
||||||
|
// which will trigger the CSS theme rules
|
||||||
|
$panelContainer.attr('data-theme', theme);
|
||||||
|
}
|
||||||
|
// For 'default', we do nothing - it will use the CSS variables from .rpg-panel class
|
||||||
|
// which fall back to SillyTavern's theme variables
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies custom colors when custom theme is selected.
|
||||||
|
*/
|
||||||
|
export function applyCustomTheme() {
|
||||||
|
if (!$panelContainer) return;
|
||||||
|
|
||||||
|
const colors = extensionSettings.customColors;
|
||||||
|
|
||||||
|
// Apply custom CSS variables as inline styles
|
||||||
|
$panelContainer.css({
|
||||||
|
'--rpg-bg': colors.bg,
|
||||||
|
'--rpg-accent': colors.accent,
|
||||||
|
'--rpg-text': colors.text,
|
||||||
|
'--rpg-highlight': colors.highlight,
|
||||||
|
'--rpg-border': colors.highlight,
|
||||||
|
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of custom color pickers.
|
||||||
|
*/
|
||||||
|
export function toggleCustomColors() {
|
||||||
|
const isCustom = extensionSettings.theme === 'custom';
|
||||||
|
$('#rpg-custom-colors').toggle(isCustom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles animations on/off by adding/removing a class to the panel.
|
||||||
|
*/
|
||||||
|
export function toggleAnimations() {
|
||||||
|
if (extensionSettings.enableAnimations) {
|
||||||
|
$panelContainer.addClass('rpg-animations-enabled');
|
||||||
|
} else {
|
||||||
|
$panelContainer.removeClass('rpg-animations-enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the settings popup theme in real-time.
|
||||||
|
* Backwards compatible wrapper for SettingsModal class.
|
||||||
|
* @param {Object} settingsModal - The SettingsModal instance (passed as parameter to avoid circular dependency)
|
||||||
|
*/
|
||||||
|
export function updateSettingsPopupTheme(settingsModal) {
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.updateTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies custom theme colors to the settings popup.
|
||||||
|
* Backwards compatible wrapper for SettingsModal class.
|
||||||
|
* @deprecated Use settingsModal.updateTheme() instead
|
||||||
|
* @param {Object} settingsModal - The SettingsModal instance (passed as parameter to avoid circular dependency)
|
||||||
|
*/
|
||||||
|
export function applyCustomThemeToSettingsPopup(settingsModal) {
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal._applyCustomTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user