10cfe581ac
Add a new Equipment tab to manage player gear and stat bonuses. Features: - 19 equipment slots across 8 categories (helmet, necklace, body armor, gloves, pants, shoes, rings, accessories) - Type-to-slot validation: each type has max equipped limits (1 helmet, 10 rings, 3 accessories, etc.) - Auto-slot assignment: equipping a ring fills the first available ring slot - Stat bonuses from equipped items display on RPG attributes (e.g. STR 10 +2) - Create/edit modal with stat checkboxes per RPG attribute - Inventory list for unequipped items Architecture: - Shared constants in src/systems/equipment/constants.js - Category-based types (Ring, Accessory) with auto-slot assignment - v7 migration converts legacy slot-specific types to generic categories - Full i18n support for all UI strings Files: - New: src/systems/equipment/constants.js - New: src/systems/interaction/equipmentActions.js - New: src/systems/rendering/equipment.js - Modified: state.js, persistence.js, template.html, index.js - Modified: userStats.js, desktop.js, mobile.js, layout.js, modals.js - Modified: apiClient.js, sillytavern.js, style.css, en.json
474 lines
18 KiB
JavaScript
474 lines
18 KiB
JavaScript
/**
|
|
* Layout Management Module
|
|
* Handles panel visibility, section visibility, collapse/expand toggle, and panel positioning
|
|
*/
|
|
|
|
import {
|
|
extensionSettings,
|
|
$panelContainer,
|
|
$userStatsContainer,
|
|
$infoBoxContainer,
|
|
$thoughtsContainer,
|
|
$inventoryContainer,
|
|
$questsContainer,
|
|
$musicPlayerContainer,
|
|
setInventoryContainer,
|
|
setQuestsContainer,
|
|
lastGeneratedData,
|
|
committedTrackerData
|
|
} from '../../core/state.js';
|
|
import { i18n } from '../../core/i18n.js';
|
|
import { setupMobileTabs, removeMobileTabs } from './mobile.js';
|
|
import { setupDesktopTabs, removeDesktopTabs, updateStripWidgets } from './desktop.js';
|
|
|
|
/**
|
|
* Toggles the visibility of plot buttons based on settings.
|
|
*/
|
|
export function togglePlotButtons() {
|
|
if (!extensionSettings.enabled) {
|
|
$('#rpg-plot-buttons').hide();
|
|
return;
|
|
}
|
|
|
|
// Show/hide randomized plot button based on enableRandomizedPlot setting
|
|
if (extensionSettings.enableRandomizedPlot) {
|
|
$('#rpg-plot-random').show();
|
|
} else {
|
|
$('#rpg-plot-random').hide();
|
|
}
|
|
|
|
// Show/hide natural plot button based on enableNaturalPlot setting
|
|
if (extensionSettings.enableNaturalPlot) {
|
|
$('#rpg-plot-natural').show();
|
|
} else {
|
|
$('#rpg-plot-natural').hide();
|
|
}
|
|
|
|
// Show/hide encounter button independently based on encounter settings
|
|
if (extensionSettings.encounterSettings?.enabled) {
|
|
$('#rpg-encounter-button').show();
|
|
} else {
|
|
$('#rpg-encounter-button').hide();
|
|
}
|
|
|
|
// Show the container if at least one button is visible
|
|
const shouldShowContainer = extensionSettings.enableRandomizedPlot || extensionSettings.enableNaturalPlot || extensionSettings.encounterSettings?.enabled;
|
|
if (shouldShowContainer) {
|
|
$('#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: icon direction based on panel position and open state
|
|
const isOpen = $panel.hasClass('rpg-mobile-open');
|
|
const isLeftPanel = $panel.hasClass('rpg-position-left');
|
|
|
|
// console.log('[RPG Mobile] updateCollapseToggleIcon:', {
|
|
// isMobile: true,
|
|
// isOpen,
|
|
// isLeftPanel,
|
|
// settingIcon: isOpen ? (isLeftPanel ? 'chevron-left' : 'chevron-right') : (isLeftPanel ? 'chevron-right' : 'chevron-left')
|
|
// });
|
|
|
|
if (isLeftPanel) {
|
|
if (isOpen) {
|
|
// Left panel open - chevron points left (panel will slide left to close)
|
|
$icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-right').addClass('fa-chevron-left');
|
|
} else {
|
|
// Left panel closed - chevron points left (panel is hidden on left)
|
|
$icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-right').addClass('fa-chevron-left');
|
|
}
|
|
} else {
|
|
// Right panel (default)
|
|
if (isOpen) {
|
|
// Right panel open - chevron points right (panel will slide right to close)
|
|
$icon.removeClass('fa-chevron-down fa-chevron-up fa-chevron-left').addClass('fa-chevron-right');
|
|
} else {
|
|
// Right panel closed - chevron points right (panel is hidden on 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');
|
|
$collapseToggle.attr('title', i18n.getTranslation('template.mainPanel.collapseExpand') || 'Collapse/Expand panel');
|
|
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');
|
|
}
|
|
|
|
// Update strip widgets when collapsing (they show in collapsed state)
|
|
updateStripWidgets();
|
|
}
|
|
});
|
|
|
|
// 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
|
|
$('#rpg-mobile-toggle').show(); // Show mobile FAB toggle
|
|
$('#rpg-collapse-toggle').show(); // Show collapse toggle
|
|
} else {
|
|
$panelContainer.hide();
|
|
$('#rpg-plot-buttons').hide(); // Hide plot buttons when disabled
|
|
$('#rpg-mobile-toggle').hide(); // Hide mobile FAB toggle
|
|
$('#rpg-collapse-toggle').hide(); // Hide collapse toggle
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the visibility of individual sections.
|
|
*/
|
|
export function updateSectionVisibility() {
|
|
// Refresh container references first (in case they were detached during tab operations)
|
|
setInventoryContainer($('#rpg-inventory'));
|
|
setQuestsContainer($('#rpg-quests'));
|
|
|
|
// Show/hide sections based on settings
|
|
// Use explicit .show()/.hide() instead of .toggle() to ensure proper state on reload
|
|
if (extensionSettings.showUserStats) {
|
|
$userStatsContainer.show();
|
|
} else {
|
|
$userStatsContainer.hide();
|
|
}
|
|
|
|
if (extensionSettings.showInfoBox) {
|
|
// Only show if there's data to display
|
|
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox;
|
|
if (infoBoxData) {
|
|
$infoBoxContainer.show();
|
|
} else {
|
|
$infoBoxContainer.hide();
|
|
}
|
|
} else {
|
|
$infoBoxContainer.hide();
|
|
}
|
|
|
|
if (extensionSettings.showCharacterThoughts) {
|
|
$thoughtsContainer.show();
|
|
} else {
|
|
$thoughtsContainer.hide();
|
|
}
|
|
|
|
// Use direct DOM selectors for inventory and quests to avoid stale references
|
|
if (extensionSettings.showInventory) {
|
|
$('#rpg-inventory').show();
|
|
} else {
|
|
$('#rpg-inventory').hide();
|
|
}
|
|
|
|
if (extensionSettings.showQuests) {
|
|
$('#rpg-quests').show();
|
|
} else {
|
|
$('#rpg-quests').hide();
|
|
}
|
|
|
|
if (extensionSettings.showEquipment) {
|
|
$('#rpg-equipment').show();
|
|
} else {
|
|
$('#rpg-equipment').hide();
|
|
}
|
|
|
|
if ($musicPlayerContainer) {
|
|
if (extensionSettings.enableSpotifyMusic) {
|
|
$musicPlayerContainer.show();
|
|
} else {
|
|
$musicPlayerContainer.hide();
|
|
}
|
|
}
|
|
|
|
// 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 || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
|
if (showDividerAfterStats) {
|
|
$('#rpg-divider-stats').show();
|
|
} else {
|
|
$('#rpg-divider-stats').hide();
|
|
}
|
|
|
|
// Divider after Info Box: shown if Info Box is visible AND at least one section after it is visible
|
|
const showDividerAfterInfo = extensionSettings.showInfoBox &&
|
|
(extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests);
|
|
if (showDividerAfterInfo) {
|
|
$('#rpg-divider-info').show();
|
|
} else {
|
|
$('#rpg-divider-info').hide();
|
|
}
|
|
|
|
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
|
|
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
|
|
(extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
|
if (showDividerAfterThoughts) {
|
|
$('#rpg-divider-thoughts').show();
|
|
} else {
|
|
$('#rpg-divider-thoughts').hide();
|
|
}
|
|
|
|
// Divider after Inventory: shown if Inventory is visible AND (Equipment, Quests or Music) is visible
|
|
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
|
if (showDividerAfterInventory) {
|
|
$('#rpg-divider-inventory').show();
|
|
} else {
|
|
$('#rpg-divider-inventory').hide();
|
|
}
|
|
|
|
// Divider after Equipment: shown if Equipment is visible AND (Quests or Music) is visible
|
|
const showDividerAfterEquipment = extensionSettings.showEquipment && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
|
if (showDividerAfterEquipment) {
|
|
$('#rpg-divider-equipment').show();
|
|
} else {
|
|
$('#rpg-divider-equipment').hide();
|
|
}
|
|
|
|
// Divider after Quests: shown if Quests is visible AND Music is visible
|
|
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
|
|
if (showDividerAfterQuests) {
|
|
$('#rpg-divider-quests').show();
|
|
} else {
|
|
$('#rpg-divider-quests').hide();
|
|
}
|
|
|
|
// Rebuild tabs to reflect visibility changes for inventory and quests
|
|
const isMobile = window.innerWidth <= 1000;
|
|
const hasMobileTabs = $('.rpg-mobile-container').length > 0;
|
|
const hasDesktopTabs = $('.rpg-tabs-nav').length > 0;
|
|
|
|
// Only rebuild if tabs currently exist
|
|
if (hasMobileTabs || hasDesktopTabs) {
|
|
// Remove existing tabs
|
|
if (hasMobileTabs) {
|
|
removeMobileTabs();
|
|
// Force remove any lingering mobile tab elements (but not the content sections!)
|
|
$('.rpg-mobile-container').remove();
|
|
$('.rpg-mobile-tabs').remove();
|
|
} else {
|
|
removeDesktopTabs();
|
|
// Force remove any lingering desktop tab structure (but not the content sections!)
|
|
// The removeDesktopTabs() function already detached and restored the sections
|
|
}
|
|
|
|
// Rebuild tabs immediately
|
|
if (isMobile) {
|
|
setupMobileTabs();
|
|
} else {
|
|
setupDesktopTabs();
|
|
}
|
|
|
|
// Refresh container references
|
|
setInventoryContainer($('#rpg-inventory'));
|
|
setQuestsContainer($('#rpg-quests'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
$('body').removeClass('rpg-panel-position-left rpg-panel-position-right rpg-panel-position-top');
|
|
|
|
// Add the appropriate position class
|
|
$panelContainer.addClass(`rpg-position-${extensionSettings.panelPosition}`);
|
|
|
|
// On mobile, also add body class for mobile-specific CSS
|
|
if (isMobile) {
|
|
$('body').addClass(`rpg-panel-position-${extensionSettings.panelPosition}`);
|
|
updateCollapseToggleIcon();
|
|
return;
|
|
}
|
|
|
|
// Desktop: Update collapse toggle icon direction for new position
|
|
updateCollapseToggleIcon();
|
|
}
|
|
|
|
/**
|
|
* Updates the UI based on generation mode selection.
|
|
*/
|
|
export function updateGenerationModeUI() {
|
|
if (extensionSettings.generationMode === 'together') {
|
|
// In "together" mode, manual update button is hidden
|
|
$('#rpg-manual-update').hide();
|
|
$('#rpg-strip-refresh').hide();
|
|
$('#rpg-external-api-settings').slideUp(200);
|
|
$('#rpg-separate-mode-settings').slideUp(200);
|
|
// Hide auto-update toggle (not applicable in together mode)
|
|
$('#rpg-auto-update-container').slideUp(200);
|
|
} else if (extensionSettings.generationMode === 'separate') {
|
|
// In "separate" mode, manual update button is visible
|
|
$('#rpg-manual-update').show();
|
|
$('#rpg-strip-refresh').show();
|
|
$('#rpg-external-api-settings').slideUp(200);
|
|
$('#rpg-separate-mode-settings').slideDown(200);
|
|
// Show auto-update toggle
|
|
$('#rpg-auto-update-container').slideDown(200);
|
|
} else if (extensionSettings.generationMode === 'external') {
|
|
// In "external" mode, manual update button is visible AND both settings are shown
|
|
$('#rpg-manual-update').show();
|
|
$('#rpg-strip-refresh').show();
|
|
$('#rpg-external-api-settings').slideDown(200);
|
|
$('#rpg-separate-mode-settings').slideDown(200);
|
|
// Show auto-update toggle for external mode too
|
|
$('#rpg-auto-update-container').slideDown(200);
|
|
}
|
|
}
|