Release v3.0.0 - Major update with JSON format, lock/unlock trackers, reorganized UI, colored dialogues, editable prompts, and numerous bug fixes
This commit is contained in:
@@ -12,6 +12,25 @@ import {
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { isItemLocked } from '../generation/lockManager.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
|
||||
/**
|
||||
* Helper to generate lock icon HTML if setting is enabled
|
||||
* @param {string} tracker - Tracker name
|
||||
* @param {string} path - Item path
|
||||
* @returns {string} Lock icon HTML or empty string
|
||||
*/
|
||||
function getLockIconHtml(tracker, path) {
|
||||
const showLockIcons = extensionSettings.showLockIcons ?? true;
|
||||
if (!showLockIcons) return '';
|
||||
|
||||
const isLocked = isItemLocked(tracker, path);
|
||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||
const lockTitle = isLocked ? 'Locked' : 'Unlocked';
|
||||
const lockedClass = isLocked ? ' locked' : '';
|
||||
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
@@ -56,41 +75,36 @@ function separateEmojiFromText(str) {
|
||||
* Includes event listeners for editable fields.
|
||||
*/
|
||||
export function renderInfoBox() {
|
||||
if (!extensionSettings.showInfoBox || !$infoBoxContainer) {
|
||||
return;
|
||||
}
|
||||
console.log('[RPG InfoBox Render] ==================== RENDERING INFO BOX ====================');
|
||||
console.log('[RPG InfoBox Render] showInfoBox setting:', extensionSettings.showInfoBox);
|
||||
console.log('[RPG InfoBox Render] Container exists:', !!$infoBoxContainer);
|
||||
|
||||
// Add updating class for animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
$infoBoxContainer.addClass('rpg-content-updating');
|
||||
if (!extensionSettings.showInfoBox || !$infoBoxContainer) {
|
||||
console.log('[RPG InfoBox Render] Exiting: showInfoBox or container is false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox;
|
||||
console.log('[RPG InfoBox Render] infoBoxData length:', infoBoxData ? infoBoxData.length : 'null');
|
||||
console.log('[RPG InfoBox Render] infoBoxData preview:', infoBoxData ? infoBoxData.substring(0, 200) : 'null');
|
||||
|
||||
// If no data yet, show placeholder
|
||||
// If no data yet, hide the container (e.g., after cache clear)
|
||||
if (!infoBoxData) {
|
||||
const placeholderHtml = `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-1">
|
||||
<div class="rpg-dashboard-widget rpg-placeholder-widget">
|
||||
<div class="rpg-placeholder-text" data-i18n-key="infobox.noData.title">${i18n.getTranslation('infobox.noData.title')}</div>
|
||||
<div class="rpg-placeholder-hint" data-i18n-key="infobox.noData.instruction">${i18n.getTranslation('infobox.noData.instruction')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$infoBoxContainer.html(placeholderHtml);
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500);
|
||||
}
|
||||
console.log('[RPG InfoBox Render] No data, hiding container');
|
||||
$infoBoxContainer.empty().hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show container and add updating class for animation
|
||||
$infoBoxContainer.show();
|
||||
if (extensionSettings.enableAnimations) {
|
||||
$infoBoxContainer.addClass('rpg-content-updating');
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] renderInfoBox called with data:', infoBoxData);
|
||||
|
||||
// Parse the info box data
|
||||
const lines = infoBoxData.split('\n');
|
||||
// console.log('[RPG Companion] Info Box split into lines:', lines);
|
||||
const data = {
|
||||
let data = {
|
||||
date: '',
|
||||
weekday: '',
|
||||
month: '',
|
||||
@@ -105,6 +119,45 @@ export function renderInfoBox() {
|
||||
characters: []
|
||||
};
|
||||
|
||||
// Check if data is v3 JSON format
|
||||
const trimmed = infoBoxData.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
const jsonData = repairJSON(infoBoxData);
|
||||
if (jsonData) {
|
||||
// Extract from v3 JSON structure
|
||||
data.weatherEmoji = jsonData.weather?.emoji || '';
|
||||
data.weatherForecast = jsonData.weather?.forecast || '';
|
||||
data.temperature = jsonData.temperature ? `${jsonData.temperature.value}°${jsonData.temperature.unit}` : '';
|
||||
data.tempValue = jsonData.temperature?.value || 0;
|
||||
data.timeStart = jsonData.time?.start || '';
|
||||
data.timeEnd = jsonData.time?.end || '';
|
||||
data.location = jsonData.location?.value || '';
|
||||
|
||||
// Parse date string to extract weekday, month, year
|
||||
if (jsonData.date?.value) {
|
||||
data.date = jsonData.date.value;
|
||||
// Expected format: "Tuesday, October 17th, 2023"
|
||||
const dateParts = data.date.split(',').map(p => p.trim());
|
||||
data.weekday = dateParts[0] || '';
|
||||
data.month = dateParts[1] || '';
|
||||
data.year = dateParts[2] || '';
|
||||
}
|
||||
|
||||
// Skip to rendering
|
||||
} else {
|
||||
// JSON parsing failed, fall back to text parsing
|
||||
parseTextFormat();
|
||||
}
|
||||
} else {
|
||||
// Text format
|
||||
parseTextFormat();
|
||||
}
|
||||
|
||||
function parseTextFormat() {
|
||||
// Parse the info box data
|
||||
const lines = infoBoxData.split('\n');
|
||||
// console.log('[RPG Companion] Info Box split into lines:', lines);
|
||||
|
||||
// Track which fields we've already parsed to avoid duplicates from mixed formats
|
||||
const parsedFields = {
|
||||
date: false,
|
||||
@@ -270,6 +323,7 @@ export function renderInfoBox() {
|
||||
// timeStart: data.timeStart,
|
||||
// location: data.location
|
||||
// });
|
||||
}
|
||||
|
||||
// Get tracker configuration
|
||||
const config = extensionSettings.trackerConfig?.infoBox;
|
||||
@@ -303,8 +357,11 @@ export function renderInfoBox() {
|
||||
weekdayDisplay = weekdayDisplay;
|
||||
}
|
||||
|
||||
const dateLockIconHtml = getLockIconHtml('infoBox', 'date');
|
||||
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||
${dateLockIconHtml}
|
||||
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthDisplay}</div>
|
||||
<div class="rpg-calendar-day rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}" title="Click to edit">${weekdayDisplay}</div>
|
||||
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
|
||||
@@ -316,8 +373,11 @@ export function renderInfoBox() {
|
||||
if (config?.widgets?.weather?.enabled) {
|
||||
const weatherEmoji = data.weatherEmoji || '🌤️';
|
||||
const weatherForecast = data.weatherForecast || 'Weather';
|
||||
const weatherLockIconHtml = getLockIconHtml('infoBox', 'weather');
|
||||
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||
${weatherLockIconHtml}
|
||||
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
|
||||
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
|
||||
</div>
|
||||
@@ -357,8 +417,11 @@ export function renderInfoBox() {
|
||||
const tempInCelsius = preferredUnit === 'F' ? Math.round((tempValue - 32) * 5/9) : tempValue;
|
||||
const tempPercent = Math.min(100, Math.max(0, ((tempInCelsius + 20) / 60) * 100));
|
||||
const tempColor = tempInCelsius < 10 ? '#4a90e2' : tempInCelsius < 25 ? '#67c23a' : '#e94560';
|
||||
const tempLockIconHtml = getLockIconHtml('infoBox', 'temperature');
|
||||
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-temp-widget">
|
||||
${tempLockIconHtml}
|
||||
<div class="rpg-thermometer">
|
||||
<div class="rpg-thermometer-bulb"></div>
|
||||
<div class="rpg-thermometer-tube">
|
||||
@@ -372,7 +435,12 @@ export function renderInfoBox() {
|
||||
|
||||
// Time widget - show if enabled
|
||||
if (config?.widgets?.time?.enabled) {
|
||||
// Determine which time value to display and edit
|
||||
const hasTimeEnd = Boolean(data.timeEnd);
|
||||
const hasTimeStart = Boolean(data.timeStart);
|
||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
const timeField = hasTimeEnd ? 'timeEnd' : 'timeStart';
|
||||
|
||||
// Parse time for clock hands
|
||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
||||
let hourAngle = 0;
|
||||
@@ -383,8 +451,12 @@ export function renderInfoBox() {
|
||||
hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||
minuteAngle = minutes * 6; // 6° per minute
|
||||
}
|
||||
|
||||
const timeLockIconHtml = getLockIconHtml('infoBox', 'time');
|
||||
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-clock-widget">
|
||||
${timeLockIconHtml}
|
||||
<div class="rpg-clock">
|
||||
<div class="rpg-clock-face">
|
||||
<div class="rpg-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
|
||||
@@ -392,7 +464,7 @@ export function renderInfoBox() {
|
||||
<div class="rpg-clock-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit">${timeDisplay}</div>
|
||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="${timeField}" title="Click to edit">${timeDisplay}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
@@ -407,9 +479,12 @@ export function renderInfoBox() {
|
||||
// Row 2: Location widget (full width) - show if enabled
|
||||
if (config?.widgets?.location?.enabled) {
|
||||
const locationDisplay = data.location || 'Location';
|
||||
const locationLockIconHtml = getLockIconHtml('infoBox', 'location');
|
||||
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-2">
|
||||
<div class="rpg-dashboard-widget rpg-location-widget">
|
||||
${locationLockIconHtml}
|
||||
<div class="rpg-map-bg">
|
||||
<div class="rpg-map-marker">📍</div>
|
||||
</div>
|
||||
@@ -421,14 +496,26 @@ export function renderInfoBox() {
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - show if enabled
|
||||
if (config?.widgets?.recentEvents?.enabled) {
|
||||
// Parse Recent Events from infoBox string
|
||||
// Parse Recent Events from infoBox (supports both JSON and text formats)
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
|
||||
// Try JSON format first
|
||||
try {
|
||||
const parsed = typeof committedTrackerData.infoBox === 'string'
|
||||
? JSON.parse(committedTrackerData.infoBox)
|
||||
: committedTrackerData.infoBox;
|
||||
|
||||
if (parsed && Array.isArray(parsed.recentEvents)) {
|
||||
recentEvents = parsed.recentEvents;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall back to old text format
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,9 +527,12 @@ export function renderInfoBox() {
|
||||
validEvents.push('Click to add event');
|
||||
}
|
||||
|
||||
const eventsLockIconHtml = getLockIconHtml('infoBox', 'recentEvents');
|
||||
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-3">
|
||||
<div class="rpg-dashboard-widget rpg-events-widget">
|
||||
${eventsLockIconHtml}
|
||||
<div class="rpg-notebook-header">
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
@@ -517,6 +607,30 @@ export function renderInfoBox() {
|
||||
}
|
||||
});
|
||||
|
||||
// Add event handler for lock icons (support both click and touch)
|
||||
$infoBoxContainer.find('.rpg-section-lock-icon').on('click touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $lockIcon = $(this);
|
||||
const tracker = $lockIcon.data('tracker');
|
||||
const path = $lockIcon.data('path');
|
||||
|
||||
// Import lockManager dynamically to avoid circular dependencies
|
||||
import('../generation/lockManager.js').then(({ setItemLock, isItemLocked }) => {
|
||||
const isLocked = isItemLocked(tracker, path);
|
||||
const newLockState = !isLocked;
|
||||
setItemLock(tracker, path, newLockState);
|
||||
|
||||
// Update icon
|
||||
$lockIcon.text(newLockState ? '🔒' : '🔓');
|
||||
$lockIcon.attr('title', newLockState ? 'Locked - AI cannot change this' : 'Unlocked - AI can change this');
|
||||
$lockIcon.toggleClass('locked', newLockState);
|
||||
|
||||
// Save settings to persist lock state
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Remove updating class after animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500);
|
||||
@@ -541,6 +655,64 @@ export function updateInfoBoxField(field, value) {
|
||||
lastGeneratedData.infoBox = 'Info Box\n---\n';
|
||||
}
|
||||
|
||||
// Check if data is in v3 JSON format
|
||||
const trimmed = lastGeneratedData.infoBox.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
// Handle v3 JSON format
|
||||
const jsonData = repairJSON(lastGeneratedData.infoBox);
|
||||
if (jsonData) {
|
||||
// Update the appropriate field based on v3 structure
|
||||
if (field === 'weatherEmoji') {
|
||||
if (!jsonData.weather) jsonData.weather = {};
|
||||
jsonData.weather.emoji = value;
|
||||
} else if (field === 'weatherForecast') {
|
||||
if (!jsonData.weather) jsonData.weather = {};
|
||||
jsonData.weather.forecast = value;
|
||||
} else if (field === 'temperature') {
|
||||
// Parse temperature value and unit
|
||||
const tempMatch = value.match(/(-?\d+)\s*°?\s*([CF]?)/i);
|
||||
if (tempMatch) {
|
||||
if (!jsonData.temperature) jsonData.temperature = {};
|
||||
jsonData.temperature.value = parseInt(tempMatch[1]);
|
||||
jsonData.temperature.unit = (tempMatch[2] || 'C').toUpperCase();
|
||||
}
|
||||
} else if (field === 'timeStart') {
|
||||
if (!jsonData.time) jsonData.time = {};
|
||||
jsonData.time.start = value;
|
||||
} else if (field === 'timeEnd') {
|
||||
if (!jsonData.time) jsonData.time = {};
|
||||
jsonData.time.end = value;
|
||||
} else if (field === 'location') {
|
||||
if (!jsonData.location) jsonData.location = {};
|
||||
jsonData.location.value = value;
|
||||
} else if (field === 'weekday' || field === 'month' || field === 'year') {
|
||||
// Update date components
|
||||
if (!jsonData.date) jsonData.date = {};
|
||||
let currentDate = jsonData.date.value || '';
|
||||
const dateParts = currentDate.split(',').map(p => p.trim());
|
||||
|
||||
if (field === 'weekday') {
|
||||
dateParts[0] = value;
|
||||
} else if (field === 'month') {
|
||||
dateParts[1] = value;
|
||||
} else if (field === 'year') {
|
||||
dateParts[2] = value;
|
||||
}
|
||||
|
||||
jsonData.date.value = dateParts.filter(p => p).join(', ');
|
||||
}
|
||||
|
||||
// Save back as JSON
|
||||
lastGeneratedData.infoBox = JSON.stringify(jsonData, null, 2);
|
||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
||||
saveChatData();
|
||||
renderInfoBox();
|
||||
console.log('[RPG Companion] Updated info box field (v3 JSON):', { field, value });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to text format handling
|
||||
// Reconstruct the Info Box text with updated field
|
||||
const lines = lastGeneratedData.infoBox.split('\n');
|
||||
let dateLineFound = false;
|
||||
|
||||
@@ -4,14 +4,32 @@
|
||||
*/
|
||||
|
||||
import { extensionSettings, $inventoryContainer } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inventoryActions.js';
|
||||
import { updateInventoryItem } from '../interaction/inventoryEdit.js';
|
||||
import { parseItems } from '../../utils/itemParser.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
|
||||
/**
|
||||
* Helper to generate lock icon HTML if setting is enabled
|
||||
* @param {string} tracker - Tracker name
|
||||
* @param {string} path - Item path
|
||||
* @returns {string} Lock icon HTML or empty string
|
||||
*/
|
||||
function getLockIconHtml(tracker, path) {
|
||||
const showLockIcons = extensionSettings.showLockIcons ?? true;
|
||||
if (!showLockIcons) return '';
|
||||
|
||||
const isLocked = isItemLocked(tracker, path);
|
||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||
const lockTitle = isLocked ? 'Locked' : 'Unlocked';
|
||||
const lockedClass = isLocked ? ' locked' : '';
|
||||
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a location name to a safe ID for use in HTML element IDs.
|
||||
* Must match the logic used in inventoryActions.js.
|
||||
@@ -31,17 +49,17 @@ export function getLocationId(locationName) {
|
||||
export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||
return `
|
||||
<div class="rpg-inventory-subtabs">
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson" data-i18n-key="inventory.section.onPerson">
|
||||
${i18n.getTranslation('inventory.section.onPerson')}
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
|
||||
On Person
|
||||
</button>
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'clothing' ? 'active' : ''}" data-tab="clothing" data-i18n-key="inventory.section.clothing">
|
||||
${i18n.getTranslation('inventory.section.clothing')}
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'clothing' ? 'active' : ''}" data-tab="clothing">
|
||||
Clothing
|
||||
</button>
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored" data-i18n-key="inventory.section.stored">
|
||||
${i18n.getTranslation('inventory.section.stored')}
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
|
||||
Stored
|
||||
</button>
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets" data-i18n-key="inventory.section.assets">
|
||||
${i18n.getTranslation('inventory.section.assets')}
|
||||
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
|
||||
Assets
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -58,28 +76,34 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.onPerson.empty">${i18n.getTranslation('inventory.onPerson.empty')}</div>`;
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No items carried</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,30 +112,30 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
return `
|
||||
<div class="rpg-inventory-section" data-section="onPerson">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4 data-i18n-key="inventory.onPerson.title">${i18n.getTranslation('inventory.onPerson.title')}</h4>
|
||||
<h4>Items Currently Carried</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="List view">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.onPerson.addItemButton">${i18n.getTranslation('inventory.onPerson.addItemButton')}</span>
|
||||
<i class="fa-solid fa-plus"></i> Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-onPerson" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-onPerson" placeholder="${i18n.getTranslation('inventory.onPerson.addItemPlaceholder')}" data-i18n-placeholder-key="inventory.onPerson.addItemPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-onPerson" placeholder="Enter item name..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="onPerson">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="onPerson">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,28 +158,34 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.clothing.empty">${i18n.getTranslation('inventory.clothing.empty')}</div>`;
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No clothing worn</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="clothing" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="clothing" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,30 +194,30 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
||||
return `
|
||||
<div class="rpg-inventory-section" data-section="clothing">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4 data-i18n-key="inventory.clothing.title">${i18n.getTranslation('inventory.clothing.title')}</h4>
|
||||
<h4>Clothing Worn</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="list" title="List view">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="clothing" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.clothing.addItemButton">${i18n.getTranslation('inventory.clothing.addItemButton')}</span>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="clothing" title="Add new clothing item">
|
||||
<i class="fa-solid fa-plus"></i> Add Clothing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-clothing" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-clothing" placeholder="${i18n.getTranslation('inventory.clothing.addItemPlaceholder')}" data-i18n-placeholder-key="inventory.clothing.addItemPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-clothing" placeholder="Enter clothing item..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="clothing">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="clothing">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,30 +242,30 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
let html = `
|
||||
<div class="rpg-inventory-section" data-section="stored">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4 data-i18n-key="inventory.stored.title">${i18n.getTranslation('inventory.stored.title')}</h4>
|
||||
<h4>Storage Locations</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="List view">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add new storage location">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.stored.addLocationButton">${i18n.getTranslation('inventory.stored.addLocationButton')}</span>
|
||||
<i class="fa-solid fa-plus"></i> Add Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-location-form" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-location-name" placeholder="${i18n.getTranslation('inventory.stored.addLocationPlaceholder')}" data-i18n-placeholder-key="inventory.stored.addLocationPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-location-name" placeholder="Enter location name..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-location">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-location">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="inventory.stored.saveButton">${i18n.getTranslation('inventory.stored.saveButton')}</span>
|
||||
<i class="fa-solid fa-check"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,8 +273,8 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
|
||||
if (locations.length === 0) {
|
||||
html += `
|
||||
<div class="rpg-inventory-empty" data-i18n-key="inventory.stored.empty">
|
||||
${i18n.getTranslation('inventory.stored.empty')}
|
||||
<div class="rpg-inventory-empty">
|
||||
No storage locations yet. Click "Add Location" to create one.
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
@@ -256,28 +286,34 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.stored.noItems">${i18n.getTranslation('inventory.stored.noItems')}</div>`;
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No items stored here</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,13 +334,13 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
</div>
|
||||
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-stored-${locationId}" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input rpg-location-item-input" data-location="${escapeHtml(location)}" placeholder="${i18n.getTranslation('inventory.stored.addItemToLocationPlaceholder')}" data-i18n-placeholder-key="inventory.stored.addItemToLocationPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input rpg-location-item-input" data-location="${escapeHtml(location)}" placeholder="Enter item name..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,18 +349,18 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
</div>
|
||||
<div class="rpg-storage-add-item-container">
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="Add item to this location">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.stored.addItemButton">${i18n.getTranslation('inventory.stored.addItemButton')}</span>
|
||||
<i class="fa-solid fa-plus"></i> Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${locationId}" style="display: none;">
|
||||
<p>${i18n.getTranslation('inventory.stored.confirmRemoveLocationMessage', { location: escapeHtml(location) })}</p>
|
||||
<p>Remove "${escapeHtml(location)}"? This will delete all items stored there.</p>
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-remove-location" data-location="${escapeHtml(location)}">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-confirm" data-action="confirm-remove-location" data-location="${escapeHtml(location)}">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="inventory.stored.confirmRemoveLocationConfirmButton">${i18n.getTranslation('inventory.stored.confirmRemoveLocationConfirmButton')}</span>
|
||||
<i class="fa-solid fa-check"></i> Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -352,28 +388,34 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.assets.empty">${i18n.getTranslation('inventory.assets.empty')}</div>`;
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No assets owned</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="assets" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="${i18n.getTranslation('inventory.assets.removeAssetTitle')}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,30 +424,30 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
return `
|
||||
<div class="rpg-inventory-section" data-section="assets">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4 data-i18n-key="inventory.assets.title">${i18n.getTranslation('inventory.assets.title')}</h4>
|
||||
<h4>Vehicles, Property & Major Possessions</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="List view">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="Add new asset">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.assets.addAssetButton">${i18n.getTranslation('inventory.assets.addAssetButton')}</span>
|
||||
<i class="fa-solid fa-plus"></i> Add Asset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-assets" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-assets" placeholder="${i18n.getTranslation('inventory.assets.addAssetPlaceholder')}" data-i18n-placeholder-key="inventory.assets.addAssetPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-assets" placeholder="Enter asset name..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="assets">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="assets">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,7 +456,8 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
</div>
|
||||
<div class="rpg-inventory-hint">
|
||||
<i class="fa-solid fa-info-circle"></i>
|
||||
<span data-i18n-key="inventory.assets.description">${i18n.getTranslation('inventory.assets.description')}</span>
|
||||
Assets include vehicles (cars, motorcycles), property (homes, apartments),
|
||||
and major equipment (workshop tools, special items).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -451,7 +494,6 @@ function generateInventoryHTML(inventory, options = {}) {
|
||||
v2Inventory = {
|
||||
version: 2,
|
||||
onPerson: 'None',
|
||||
clothing: 'None',
|
||||
stored: {},
|
||||
assets: 'None'
|
||||
};
|
||||
@@ -461,9 +503,6 @@ function generateInventoryHTML(inventory, options = {}) {
|
||||
if (!v2Inventory.onPerson || typeof v2Inventory.onPerson !== 'string') {
|
||||
v2Inventory.onPerson = 'None';
|
||||
}
|
||||
if (!v2Inventory.clothing || typeof v2Inventory.clothing !== 'string') {
|
||||
v2Inventory.clothing = 'None';
|
||||
}
|
||||
if (!v2Inventory.stored || typeof v2Inventory.stored !== 'object' || Array.isArray(v2Inventory.stored)) {
|
||||
v2Inventory.stored = {};
|
||||
}
|
||||
@@ -563,6 +602,31 @@ export function renderInventory() {
|
||||
const newName = $(this).text().trim();
|
||||
updateInventoryItem(field, index, newName, location);
|
||||
});
|
||||
|
||||
// Add event listener for section lock icon clicks (support both click and touch)
|
||||
$inventoryContainer.find('.rpg-section-lock-icon').on('click touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $icon = $(this);
|
||||
const trackerType = $icon.data('tracker');
|
||||
const itemPath = $icon.data('path');
|
||||
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
||||
|
||||
// Toggle lock state
|
||||
setItemLock(trackerType, itemPath, !currentlyLocked);
|
||||
|
||||
// Update icon
|
||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
||||
$icon.text(newIcon);
|
||||
$icon.attr('title', newTitle);
|
||||
|
||||
// Toggle 'locked' class for persistent visibility
|
||||
$icon.toggleClass('locked', !currentlyLocked);
|
||||
|
||||
// Save settings
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,20 +56,20 @@ function openInSpotify(songData) {
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
export function renderMusicPlayer(container) {
|
||||
console.log('[RPG Companion] Music Player: renderMusicPlayer called');
|
||||
// console.log('[RPG Companion] Music Player: renderMusicPlayer called');
|
||||
|
||||
// Remove old chat-attached player if it exists
|
||||
$('#rpg-chat-music-player').remove();
|
||||
|
||||
console.log('[RPG Companion] Music Player: enableSpotifyMusic =', extensionSettings.enableSpotifyMusic);
|
||||
// console.log('[RPG Companion] Music Player: enableSpotifyMusic =', extensionSettings.enableSpotifyMusic);
|
||||
|
||||
if (!extensionSettings.enableSpotifyMusic) {
|
||||
console.warn('[RPG Companion] Music Player: Spotify music is disabled');
|
||||
// console.warn('[RPG Companion] Music Player: Spotify music is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const songData = committedTrackerData.spotifyUrl;
|
||||
console.log('[RPG Companion] Music Player: Rendering with song:', songData);
|
||||
// console.log('[RPG Companion] Music Player: Rendering with song:', songData);
|
||||
|
||||
if (!songData || !songData.displayText) {
|
||||
// No song - don't show anything
|
||||
|
||||
@@ -5,7 +5,24 @@
|
||||
|
||||
import { extensionSettings, $questsContainer } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
|
||||
/**
|
||||
* Helper to generate lock icon HTML if setting is enabled
|
||||
* @param {string} tracker - Tracker name
|
||||
* @param {string} path - Item path
|
||||
* @returns {string} Lock icon HTML or empty string
|
||||
*/
|
||||
function getLockIconHtml(tracker, path) {
|
||||
const showLockIcons = extensionSettings.showLockIcons ?? true;
|
||||
if (!showLockIcons) return '';
|
||||
|
||||
const isLocked = isItemLocked(tracker, path);
|
||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||
const lockTitle = isLocked ? 'Locked' : 'Unlocked';
|
||||
const lockedClass = isLocked ? ' locked' : '';
|
||||
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML escape helper
|
||||
@@ -26,11 +43,11 @@ function escapeHtml(text) {
|
||||
export function renderQuestsSubTabs(activeTab = 'main') {
|
||||
return `
|
||||
<div class="rpg-quests-subtabs">
|
||||
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main" data-i18n-key="quests.section.main">
|
||||
${i18n.getTranslation('quests.section.main')}
|
||||
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main">
|
||||
Main Quest
|
||||
</button>
|
||||
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional" data-i18n-key="quests.section.optional">
|
||||
${i18n.getTranslation('quests.section.optional')}
|
||||
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional">
|
||||
Optional Quests
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -48,9 +65,9 @@ export function renderMainQuestView(mainQuest) {
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title" data-i18n-key="quests.main.title">${i18n.getTranslation('quests.main.title')}</h3>
|
||||
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle')}">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<h3 class="rpg-quest-section-title">Main Quests</h3>
|
||||
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="Add main quests">
|
||||
<i class="fa-solid fa-plus"></i> Add Quest
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
@@ -59,14 +76,15 @@ export function renderMainQuestView(mainQuest) {
|
||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.save">${i18n.getTranslation('global.save')}</span>
|
||||
<i class="fa-solid fa-check"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-item" data-field="main">
|
||||
${getLockIconHtml('userStats', 'quests.main')}
|
||||
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest">
|
||||
@@ -79,22 +97,22 @@ export function renderMainQuestView(mainQuest) {
|
||||
</div>
|
||||
` : `
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${i18n.getTranslation('quests.main.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.main.addQuestPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="Enter main quests title..." />
|
||||
<div class="rpg-inline-actions">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-empty" data-i18n-key="quests.main.empty">${i18n.getTranslation('quests.main.empty')}</div>
|
||||
<div class="rpg-quest-empty">No active main quests</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-lightbulb"></i>
|
||||
<span data-i18n-key="quests.main.hint">${i18n.getTranslation('quests.main.hint')}</span>
|
||||
The main quests represent your primary objective in the story.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -110,10 +128,12 @@ export function renderOptionalQuestsView(optionalQuests) {
|
||||
|
||||
let questsHtml = '';
|
||||
if (quests.length === 0) {
|
||||
questsHtml = `<div class="rpg-quest-empty" data-i18n-key="quests.optional.empty">${i18n.getTranslation('quests.optional.empty')}</div>`;
|
||||
questsHtml = '<div class="rpg-quest-empty">No active optional quests</div>';
|
||||
} else {
|
||||
questsHtml = quests.map((quest, index) => `
|
||||
questsHtml = quests.map((quest, index) => {
|
||||
return `
|
||||
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
|
||||
${getLockIconHtml('userStats', `quests.optional[${index}]`)}
|
||||
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
|
||||
@@ -121,26 +141,26 @@ export function renderOptionalQuestsView(optionalQuests) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title" data-i18n-key="quests.optional.title">${i18n.getTranslation('quests.optional.title')}</h3>
|
||||
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle')}">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<h3 class="rpg-quest-section-title">Optional Quests</h3>
|
||||
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="Add optional quest">
|
||||
<i class="fa-solid fa-plus"></i> Add Quest
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="${i18n.getTranslation('quests.optional.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.optional.addQuestPlaceholder" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="Enter optional quest title..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="optional">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +169,7 @@ export function renderOptionalQuestsView(optionalQuests) {
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-info-circle"></i>
|
||||
<span data-i18n-key="quests.optional.hint">${i18n.getTranslation('quests.optional.hint')}</span>
|
||||
Optional quests are side objectives that complement your main story.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +180,7 @@ export function renderOptionalQuestsView(optionalQuests) {
|
||||
* Main render function for quests
|
||||
*/
|
||||
export function renderQuests() {
|
||||
if (!extensionSettings.showQuests || !$questsContainer) {
|
||||
if (!extensionSettings.showInventory || !$questsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,4 +324,29 @@ function attachQuestEventHandlers() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for section lock icon clicks (support both click and touch)
|
||||
$questsContainer.find('.rpg-section-lock-icon').on('click touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $icon = $(this);
|
||||
const trackerType = $icon.data('tracker');
|
||||
const itemPath = $icon.data('path');
|
||||
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
||||
|
||||
// Toggle lock state
|
||||
setItemLock(trackerType, itemPath, !currentlyLocked);
|
||||
|
||||
// Update icon
|
||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
||||
$icon.text(newIcon);
|
||||
$icon.attr('title', newTitle);
|
||||
|
||||
// Toggle 'locked' class for persistent visibility
|
||||
$icon.toggleClass('locked', !currentlyLocked);
|
||||
|
||||
// Save settings
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
+867
-535
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../core/persistence.js';
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
|
||||
/**
|
||||
* Builds the user stats text string using custom stat names
|
||||
@@ -67,6 +68,107 @@ export function buildUserStatsText() {
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lastGeneratedData.userStats and committedTrackerData.userStats
|
||||
* Maintains JSON format if current data is JSON, otherwise uses text format.
|
||||
* @private
|
||||
*/
|
||||
function updateUserStatsData() {
|
||||
// Check if current data is in JSON format
|
||||
const currentData = lastGeneratedData.userStats || committedTrackerData.userStats;
|
||||
if (currentData) {
|
||||
const trimmed = currentData.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
// Maintain JSON format
|
||||
try {
|
||||
const jsonData = JSON.parse(currentData);
|
||||
if (jsonData && typeof jsonData === 'object') {
|
||||
const stats = extensionSettings.userStats;
|
||||
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||
const enabledStats = config.customStats?.filter(stat => stat && stat.enabled && stat.name && stat.id) || [];
|
||||
|
||||
// Build stats array - include all stats from extensionSettings, not just enabled ones
|
||||
// This preserves custom stats that AI might have added or that user has disabled
|
||||
const statsArray = [];
|
||||
const processedIds = new Set();
|
||||
|
||||
// First, add all enabled stats from config (maintains order)
|
||||
enabledStats.forEach(stat => {
|
||||
statsArray.push({
|
||||
id: stat.id,
|
||||
name: stat.name,
|
||||
value: stats[stat.id] !== undefined ? stats[stat.id] : 100
|
||||
});
|
||||
processedIds.add(stat.id);
|
||||
});
|
||||
|
||||
// Then, add any other numeric stats from extensionSettings that aren't in config
|
||||
// (these could be custom stats the AI added or disabled stats)
|
||||
const excludeFields = new Set(['mood', 'conditions', 'inventory', 'skills', 'level']);
|
||||
Object.entries(stats).forEach(([key, value]) => {
|
||||
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
||||
statsArray.push({
|
||||
id: key,
|
||||
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
value: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
jsonData.stats = statsArray;
|
||||
|
||||
// Update status
|
||||
jsonData.status = {
|
||||
mood: stats.mood || '😐',
|
||||
conditions: stats.conditions || 'None'
|
||||
};
|
||||
|
||||
// Update inventory (convert to v3 format)
|
||||
const convertToV3Items = (itemString) => {
|
||||
if (!itemString) return [];
|
||||
const items = itemString.split(',').map(s => s.trim()).filter(s => s);
|
||||
return items.map(item => {
|
||||
const qtyMatch = item.match(/^(\\d+)x\\s+(.+)$/);
|
||||
if (qtyMatch) {
|
||||
return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) };
|
||||
}
|
||||
return { name: item, quantity: 1 };
|
||||
});
|
||||
};
|
||||
|
||||
jsonData.inventory = {
|
||||
onPerson: convertToV3Items(stats.inventory?.onPerson),
|
||||
clothing: convertToV3Items(stats.inventory?.clothing),
|
||||
stored: stats.inventory?.stored || {},
|
||||
assets: convertToV3Items(stats.inventory?.assets)
|
||||
};
|
||||
|
||||
// Update quests
|
||||
jsonData.quests = extensionSettings.quests || { main: '', optional: [] };
|
||||
|
||||
// Update skills if present
|
||||
if (stats.skills) {
|
||||
jsonData.skills = Array.isArray(stats.skills) ? stats.skills :
|
||||
stats.skills.split(',').map(s => s.trim()).filter(s => s);
|
||||
}
|
||||
|
||||
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||
lastGeneratedData.userStats = updatedJSON;
|
||||
committedTrackerData.userStats = updatedJSON;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to parse JSON, falling back to text format:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to text format
|
||||
const statsText = buildUserStatsText();
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user stats panel with health bars, mood, inventory, and classic stats.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -77,7 +179,36 @@ export function renderUserStats() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't render if no data exists (e.g., after cache clear)
|
||||
// Check both lastGeneratedData and committedTrackerData
|
||||
console.log('[RPG UserStats Render] Checking data:', {
|
||||
hasLastGenerated: !!lastGeneratedData.userStats,
|
||||
hasCommitted: !!committedTrackerData.userStats,
|
||||
lastGeneratedPreview: lastGeneratedData.userStats ? lastGeneratedData.userStats.substring(0, 100) : 'null',
|
||||
committedPreview: committedTrackerData.userStats ? committedTrackerData.userStats.substring(0, 100) : 'null'
|
||||
});
|
||||
|
||||
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||
$userStatsContainer.html('<div class="rpg-inventory-empty">No statuses generated yet</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use lastGeneratedData if available, otherwise fall back to committed data
|
||||
if (!lastGeneratedData.userStats && committedTrackerData.userStats) {
|
||||
lastGeneratedData.userStats = committedTrackerData.userStats;
|
||||
}
|
||||
|
||||
const stats = extensionSettings.userStats;
|
||||
console.log('[RPG UserStats Render] Current extensionSettings.userStats:', {
|
||||
health: stats.health,
|
||||
satiety: stats.satiety,
|
||||
energy: stats.energy,
|
||||
hygiene: stats.hygiene,
|
||||
arousal: stats.arousal,
|
||||
mood: stats.mood,
|
||||
conditions: stats.conditions
|
||||
});
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
@@ -116,20 +247,32 @@ export function renderUserStats() {
|
||||
// Create gradient from low to high color
|
||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
||||
|
||||
let html = '<div class="rpg-stats-content"><div class="rpg-stats-left">';
|
||||
// Check if stats bars section is locked
|
||||
const isStatsLocked = isItemLocked('userStats', 'stats');
|
||||
const lockIcon = isStatsLocked ? '🔒' : '🔓';
|
||||
const lockTitle = isStatsLocked ? 'Locked - AI cannot change stats' : 'Unlocked - AI can change stats';
|
||||
const lockedClass = isStatsLocked ? ' locked' : '';
|
||||
|
||||
let html = '<div class="rpg-stats-content">';
|
||||
html += '<div class="rpg-stats-left">';
|
||||
|
||||
// User info row
|
||||
const showLevel = extensionSettings.trackerConfig?.userStats?.showLevel !== false;
|
||||
html += `
|
||||
<div class="rpg-user-info-row">
|
||||
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
<span class="rpg-user-name">${userName}</span>
|
||||
<span style="opacity: 0.5;">|</span>
|
||||
${showLevel ? `<span style="opacity: 0.5;">|</span>
|
||||
<span class="rpg-level-label">LVL</span>
|
||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>
|
||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Dynamic stats grid - only show enabled stats
|
||||
const showLockIcons = extensionSettings.showLockIcons ?? true;
|
||||
if (showLockIcons) {
|
||||
html += `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="userStats" data-path="stats" title="${lockTitle}">${lockIcon}</span>`;
|
||||
}
|
||||
html += '<div class="rpg-stats-grid">';
|
||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||
|
||||
@@ -149,7 +292,14 @@ export function renderUserStats() {
|
||||
|
||||
// Status section (conditionally rendered)
|
||||
if (config.statusSection.enabled) {
|
||||
const isMoodLocked = isItemLocked('userStats', 'status');
|
||||
const moodLockIcon = isMoodLocked ? '🔒' : '🔓';
|
||||
const moodLockTitle = isMoodLocked ? 'Locked - AI cannot change mood' : 'Unlocked - AI can change mood';
|
||||
const moodLockedClass = isMoodLocked ? ' locked' : '';
|
||||
html += '<div class="rpg-mood">';
|
||||
if (showLockIcons) {
|
||||
html += `<span class="rpg-section-lock-icon${moodLockedClass}" data-tracker="userStats" data-path="status" title="${moodLockTitle}">${moodLockIcon}</span>`;
|
||||
}
|
||||
|
||||
if (config.statusSection.showMoodEmoji) {
|
||||
html += `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>`;
|
||||
@@ -158,7 +308,11 @@ export function renderUserStats() {
|
||||
// Render custom status fields
|
||||
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
||||
// For now, use first field as "conditions" for backward compatibility
|
||||
const conditionsValue = stats.conditions || 'None';
|
||||
let conditionsValue = stats.conditions || 'None';
|
||||
// Strip brackets if present (from JSON array format)
|
||||
if (typeof conditionsValue === 'string') {
|
||||
conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
|
||||
}
|
||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
|
||||
}
|
||||
|
||||
@@ -167,9 +321,24 @@ export function renderUserStats() {
|
||||
|
||||
// Skills section (conditionally rendered)
|
||||
if (config.skillsSection.enabled) {
|
||||
const skillsValue = stats.skills || 'None';
|
||||
const isSkillsLocked = isItemLocked('userStats', 'skills');
|
||||
const skillsLockIcon = isSkillsLocked ? '🔒' : '🔓';
|
||||
const skillsLockTitle = isSkillsLocked ? 'Locked - AI cannot change skills' : 'Unlocked - AI can change skills';
|
||||
const skillsLockedClass = isSkillsLocked ? ' locked' : '';
|
||||
let skillsValue = 'None';
|
||||
// Handle JSON array format: [{name: "Art"}, {name: "Coding"}]
|
||||
if (Array.isArray(stats.skills)) {
|
||||
skillsValue = stats.skills.map(s => s.name || s).join(', ') || 'None';
|
||||
} else if (stats.skills) {
|
||||
skillsValue = stats.skills;
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-skills-section">`;
|
||||
if (showLockIcons) {
|
||||
html += `
|
||||
<span class="rpg-section-lock-icon${skillsLockedClass}" data-tracker="userStats" data-path="skills" title="${skillsLockTitle}">${skillsLockIcon}</span>`;
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-skills-section">
|
||||
<span class="rpg-skills-label">${config.skillsSection.label}:</span>
|
||||
<div class="rpg-skills-value rpg-editable" contenteditable="true" data-field="skills" title="Click to edit skills">${skillsValue}</div>
|
||||
</div>
|
||||
@@ -225,7 +394,13 @@ export function renderUserStats() {
|
||||
|
||||
html += '</div>'; // Close rpg-stats-content
|
||||
|
||||
console.log('[RPG UserStats Render] Generated HTML length:', html.length);
|
||||
console.log('[RPG UserStats Render] HTML preview:', html.substring(0, 300));
|
||||
console.log('[RPG UserStats Render] Container exists:', !!$userStatsContainer, '$userStatsContainer length:', $userStatsContainer?.length);
|
||||
|
||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||
$userStatsContainer.html(html);
|
||||
console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
|
||||
|
||||
// Add event listeners for editable stat values
|
||||
$('.rpg-editable-stat').on('blur', function() {
|
||||
@@ -242,13 +417,8 @@ export function renderUserStats() {
|
||||
// Update the setting
|
||||
extensionSettings.userStats[field] = value;
|
||||
|
||||
// Rebuild userStats text with custom stat names
|
||||
const statsText = buildUserStatsText();
|
||||
|
||||
// Update BOTH lastGeneratedData AND committedTrackerData
|
||||
// This makes manual edits immediately visible to AI
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -263,13 +433,8 @@ export function renderUserStats() {
|
||||
const value = $(this).text().trim();
|
||||
extensionSettings.userStats.mood = value || '😐';
|
||||
|
||||
// Rebuild userStats text with custom stat names
|
||||
const statsText = buildUserStatsText();
|
||||
|
||||
// Update BOTH lastGeneratedData AND committedTrackerData
|
||||
// This makes manual edits immediately visible to AI
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -280,13 +445,8 @@ export function renderUserStats() {
|
||||
const value = $(this).text().trim();
|
||||
extensionSettings.userStats.conditions = value || 'None';
|
||||
|
||||
// Rebuild userStats text with custom stat names
|
||||
const statsText = buildUserStatsText();
|
||||
|
||||
// Update BOTH lastGeneratedData AND committedTrackerData
|
||||
// This makes manual edits immediately visible to AI
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -298,12 +458,8 @@ export function renderUserStats() {
|
||||
const value = $(this).text().trim();
|
||||
extensionSettings.userStats.skills = value || 'None';
|
||||
|
||||
// Rebuild userStats text
|
||||
const statsText = buildUserStatsText();
|
||||
|
||||
// Update BOTH lastGeneratedData AND committedTrackerData
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -359,4 +515,29 @@ export function renderUserStats() {
|
||||
$(this).blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for section lock icon clicks (support both click and touch)
|
||||
$('.rpg-section-lock-icon').on('click touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $icon = $(this);
|
||||
const trackerType = $icon.data('tracker');
|
||||
const itemPath = $icon.data('path');
|
||||
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
||||
|
||||
// Toggle lock state
|
||||
setItemLock(trackerType, itemPath, !currentlyLocked);
|
||||
|
||||
// Update icon
|
||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||
const newTitle = !currentlyLocked ? 'Locked - AI cannot change this section' : 'Unlocked - AI can change this section';
|
||||
$icon.text(newIcon);
|
||||
$icon.attr('title', newTitle);
|
||||
|
||||
// Toggle 'locked' class for persistent visibility
|
||||
$icon.toggleClass('locked', !currentlyLocked);
|
||||
|
||||
// Save settings
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user