Files
rpg-companion-sillytavern/src/systems/ui/layout.js
T
ARIA 10cfe581ac feat: add Equipment tab with slot-type validation
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
2026-07-03 11:11:23 +02:00

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);
}
}