/**
* Tracker Editor Module
* Provides UI for customizing tracker configurations
*/
import { i18n } from '../../core/i18n.js';
import { extensionSettings } from '../../core/state.js';
import {
saveSettings,
getPresets,
getPreset,
getActivePresetId,
getDefaultPresetId,
setDefaultPreset,
isDefaultPreset,
createPreset,
saveToPreset,
loadPreset,
renamePreset,
deletePreset,
associatePresetWithCurrentEntity,
removePresetAssociationForCurrentEntity,
getPresetForCurrentEntity,
hasPresetAssociation,
getCurrentEntityName,
exportPresets,
importPresets
} from '../../core/persistence.js';
import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts } from '../rendering/thoughts.js';
let $editorModal = null;
let activeTab = 'userStats';
let tempConfig = null; // Temporary config for cancel functionality
/**
* Initialize the tracker editor modal
*/
export function initTrackerEditor() {
// Modal will be in template.html, just set up event listeners
$editorModal = $('#rpg-tracker-editor-popup');
if (!$editorModal.length) {
console.error('[RPG Companion] Tracker editor modal not found in template');
return;
}
// Tab switching
$(document).on('click', '.rpg-editor-tab', function() {
$('.rpg-editor-tab').removeClass('active');
$(this).addClass('active');
activeTab = $(this).data('tab');
$('.rpg-editor-tab-content').hide();
$(`#rpg-editor-tab-${activeTab}`).show();
});
// Save button
$(document).on('click', '#rpg-editor-save', function() {
applyTrackerConfig();
closeTrackerEditor();
});
// Cancel button
$(document).on('click', '#rpg-editor-cancel', function() {
closeTrackerEditor();
});
// Close X button
$(document).on('click', '#rpg-close-tracker-editor', function() {
closeTrackerEditor();
});
// Reset button
$(document).on('click', '#rpg-editor-reset', function() {
resetToDefaults();
renderEditorUI();
});
// Close on background click
$(document).on('click', '#rpg-tracker-editor-popup', function(e) {
if (e.target.id === 'rpg-tracker-editor-popup') {
closeTrackerEditor();
}
});
// Open button
$(document).on('click', '#rpg-open-tracker-editor', function() {
openTrackerEditor();
});
// Export button
$(document).on('click', '#rpg-editor-export', function() {
exportTrackerPreset();
});
// Import button
$(document).on('click', '#rpg-editor-import', function() {
importTrackerPreset();
});
// Preset select change
$(document).on('change', '#rpg-preset-select', function() {
const presetId = $(this).val();
if (presetId && presetId !== getActivePresetId()) {
// Save current changes to the old preset before switching
const currentPresetId = getActivePresetId();
if (currentPresetId) {
saveToPreset(currentPresetId);
}
// Load the new preset
if (loadPreset(presetId)) {
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
renderEditorUI();
updatePresetUI();
toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`);
}
}
});
// New preset button
$(document).on('click', '#rpg-preset-new', function() {
const name = prompt('Enter a name for the new preset:');
if (name && name.trim()) {
const newId = createPreset(name.trim());
updatePresetUI();
$('#rpg-preset-select').val(newId);
toastr.success(`Created preset "${name.trim()}".`);
}
});
// Set as default preset button
$(document).on('click', '#rpg-preset-default', function() {
const currentPresetId = getActivePresetId();
if (currentPresetId) {
setDefaultPreset(currentPresetId);
updatePresetUI();
const preset = getPreset(currentPresetId);
toastr.success(`"${preset?.name || 'Unknown'}" is now the default preset.`);
}
});
// Delete preset button
$(document).on('click', '#rpg-preset-delete', function() {
const currentPresetId = getActivePresetId();
const presets = getPresets();
if (Object.keys(presets).length <= 1) {
toastr.warning('Cannot delete the last preset.');
return;
}
const preset = getPreset(currentPresetId);
if (confirm(`Are you sure you want to delete the preset "${preset?.name || 'Unknown'}"?`)) {
if (deletePreset(currentPresetId)) {
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
renderEditorUI();
updatePresetUI();
toastr.success('Preset deleted.');
}
}
});
// Associate preset checkbox
$(document).on('change', '#rpg-preset-associate', function() {
if ($(this).is(':checked')) {
associatePresetWithCurrentEntity();
toastr.info(`This preset will be used for ${getCurrentEntityName()}.`);
} else {
removePresetAssociationForCurrentEntity();
toastr.info(`Preset association removed for ${getCurrentEntityName()}.`);
}
});
}
/**
* Updates the preset management UI (dropdown, association checkbox, entity name)
*/
function updatePresetUI() {
const presets = getPresets();
const activePresetId = getActivePresetId();
const defaultPresetId = getDefaultPresetId();
const $select = $('#rpg-preset-select');
// Populate the dropdown
$select.empty();
for (const [id, preset] of Object.entries(presets)) {
const isDefault = id === defaultPresetId;
const starPrefix = isDefault ? '★ ' : '';
$select.append(``);
}
$select.val(activePresetId);
// Update the default button appearance
const $defaultBtn = $('#rpg-preset-default');
if (isDefaultPreset(activePresetId)) {
$defaultBtn.addClass('rpg-btn-active').attr('title', 'This is the default preset');
} else {
$defaultBtn.removeClass('rpg-btn-active').attr('title', 'Set as Default Preset');
}
// Update the entity name display
const entityName = getCurrentEntityName();
$('#rpg-preset-entity-name').text(entityName);
// Update the association checkbox
const isAssociated = hasPresetAssociation();
$('#rpg-preset-associate').prop('checked', isAssociated);
}
/**
* Open the tracker editor modal
*/
function openTrackerEditor() {
// Create temporary copy for cancel functionality
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
// Set theme to match current extension theme
const theme = extensionSettings.theme || 'modern';
$editorModal.attr('data-theme', theme);
// Update preset UI
updatePresetUI();
renderEditorUI();
$editorModal.addClass('is-open').css('display', '');
}
/**
* Close the tracker editor modal
*/
function closeTrackerEditor() {
// Restore from temp if canceling
if (tempConfig) {
extensionSettings.trackerConfig = tempConfig;
tempConfig = null;
}
$editorModal.removeClass('is-open').addClass('is-closing');
setTimeout(() => {
$editorModal.removeClass('is-closing').hide();
}, 200);
}
/**
* Apply the tracker configuration and refresh all trackers
*/
function applyTrackerConfig() {
tempConfig = null; // Clear temp config
// Save to the current preset
const currentPresetId = getActivePresetId();
if (currentPresetId) {
saveToPreset(currentPresetId);
} else {
saveSettings();
}
// Re-render all trackers with new config
renderUserStats();
renderInfoBox();
renderThoughts();
}
/**
* Reset configuration to defaults
*/
function resetToDefaults() {
extensionSettings.trackerConfig = {
userStats: {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'satiety', name: 'Satiety', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true },
{ id: 'hygiene', name: 'Hygiene', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
],
statusSection: {
enabled: true,
showMoodEmoji: true,
customFields: ['Conditions']
},
skillsSection: {
enabled: false,
label: 'Skills',
customFields: []
}
},
infoBox: {
widgets: {
date: { enabled: true, format: 'Weekday, Month, Year' },
weather: { enabled: true },
temperature: { enabled: true, unit: 'C' },
time: { enabled: true },
location: { enabled: true },
recentEvents: { enabled: true }
}
},
presentCharacters: {
showEmoji: true,
showName: true,
relationships: {
enabled: true,
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
}
},
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
},
customFields: [
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' }
],
thoughts: {
enabled: true,
name: 'Thoughts',
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)'
},
characterStats: {
enabled: false,
customStats: [
{ id: 'health', name: 'Health', enabled: true, colorLow: '#ff4444', colorHigh: '#44ff44' },
{ id: 'energy', name: 'Energy', enabled: true, colorLow: '#ffaa00', colorHigh: '#44ffff' }
]
}
}
};
}
/**
* Export current tracker configuration to a JSON file
*/
function exportTrackerPreset() {
try {
// Get the current tracker configuration
const config = extensionSettings.trackerConfig;
// Create a preset object with metadata
const preset = {
name: 'Custom Tracker Preset',
version: '1.0',
exportDate: new Date().toISOString(),
trackerConfig: JSON.parse(JSON.stringify(config)) // Deep copy
};
// Convert to JSON
const jsonString = JSON.stringify(preset, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
// Create download link
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// Generate filename with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
link.download = `rpg-tracker-preset-${timestamp}.json`;
// Trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
// console.log('[RPG Companion] Tracker preset exported successfully');
toastr.success(i18n.getTranslation('template.trackerEditorModal.messages.exportSuccess') || 'Tracker preset exported successfully!');
} catch (error) {
console.error('[RPG Companion] Error exporting tracker preset:', error);
toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.exportError') || 'Failed to export tracker preset. Check console for details.');
}
}
/**
* Migrates old tracker preset format to current format
* @param {Object} config - The tracker config to migrate
* @returns {Object} - Migrated tracker config
*/
function migrateTrackerPreset(config) {
// Create a deep copy to avoid modifying the original
const migrated = JSON.parse(JSON.stringify(config));
// Migrate relationships structure (v3.0.0 -> v3.1.0)
if (migrated.presentCharacters) {
// Old format: relationshipEmojis directly on presentCharacters
// New format: relationships.relationshipEmojis
if (migrated.presentCharacters.relationshipEmojis &&
!migrated.presentCharacters.relationships) {
migrated.presentCharacters.relationships = {
enabled: migrated.presentCharacters.enableRelationships || true,
relationshipEmojis: migrated.presentCharacters.relationshipEmojis
};
// Keep legacy fields for backward compatibility
migrated.presentCharacters.relationshipFields = Object.keys(migrated.presentCharacters.relationshipEmojis);
}
// Ensure relationships object exists
if (!migrated.presentCharacters.relationships) {
migrated.presentCharacters.relationships = {
enabled: false,
relationshipEmojis: {}
};
}
// Ensure relationshipEmojis exists within relationships
if (!migrated.presentCharacters.relationships.relationshipEmojis) {
migrated.presentCharacters.relationships.relationshipEmojis = {};
}
}
// Add any other migration logic here for future format changes
return migrated;
}
/**
* Import tracker configuration from a JSON file
*/
function importTrackerPreset() {
// Create file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
// Validate the imported data
if (!data.trackerConfig) {
throw new Error('Invalid preset file: missing trackerConfig');
}
// Validate required sections
if (!data.trackerConfig.userStats || !data.trackerConfig.infoBox || !data.trackerConfig.presentCharacters) {
throw new Error('Invalid preset file: missing required configuration sections');
}
// Migrate old preset format to current format
const migratedConfig = migrateTrackerPreset(data.trackerConfig);
// Show import mode selection dialog
showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', ''));
} catch (error) {
console.error('[RPG Companion] Error importing tracker preset:', error);
toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') ||
`Failed to import tracker preset: ${error.message}`);
}
};
// Trigger file selection
input.click();
}
/**
* Show dialog to choose import mode
* @param {Object} migratedConfig - The migrated tracker config
* @param {string} suggestedName - Suggested name for new preset
*/
function showImportModeDialog(migratedConfig, suggestedName) {
// Create dialog overlay
const dialogHtml = `
Import Configuration
How would you like to import this configuration?
`;
$('body').append(dialogHtml);
const $dialog = $('#rpg-import-mode-dialog');
// Import to current preset
$('#rpg-import-to-current').on('click', () => {
$dialog.remove();
// Apply the migrated configuration to current
extensionSettings.trackerConfig = migratedConfig;
// Save to the active preset (saveToPreset uses current trackerConfig)
const activePresetId = getActivePresetId();
if (activePresetId) {
saveToPreset(activePresetId);
}
// Re-render the editor UI
renderEditorUI();
toastr.success('Configuration applied to current preset.');
});
// Import as new preset
$('#rpg-import-as-new').on('click', () => {
$dialog.remove();
// Prompt for preset name
const presetName = prompt('Enter a name for the new preset:', suggestedName);
if (!presetName) return;
// Set the migrated config as current first
extensionSettings.trackerConfig = migratedConfig;
// Create new preset (createPreset uses current trackerConfig)
const newPresetId = createPreset(presetName);
if (newPresetId) {
// Load the new preset
loadPreset(newPresetId);
renderEditorUI();
updatePresetUI();
toastr.success(`Created new preset: ${presetName}.`);
}
});
// Cancel
$('#rpg-import-cancel').on('click', () => {
$dialog.remove();
});
// Close on overlay click
$dialog.on('click', (e) => {
if (e.target === $dialog[0]) {
$dialog.remove();
}
});
}
/**
* Render the editor UI based on current config
*/
function renderEditorUI() {
renderUserStatsTab();
renderInfoBoxTab();
renderPresentCharactersTab();
}
/**
* Render User Stats configuration tab
*/
function renderUserStatsTab() {
const config = extensionSettings.trackerConfig.userStats;
let html = '
`;
// Toggle for enabling/disabling relationships
const relationshipsEnabled = config.relationships?.enabled !== false; // Default to true if not set
html += '