diff --git a/index.js b/index.js index 4a6c2ff..c32a4c3 100644 --- a/index.js +++ b/index.js @@ -131,7 +131,8 @@ import { } from './src/systems/ui/mobile.js'; import { setupDesktopTabs, - removeDesktopTabs + removeDesktopTabs, + updateStripWidgets } from './src/systems/ui/desktop.js'; // Feature modules @@ -726,6 +727,63 @@ async function initUI() { updateFabWidgets(); }); + // Desktop Strip Widget toggles + $('#rpg-toggle-strip-widgets-enabled').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + $('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled); + }); + + $('#rpg-toggle-strip-weather-icon').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {}; + extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-clock').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {}; + extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-date').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {}; + extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-location').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {}; + extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-stats').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {}; + extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-attributes').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {}; + extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + $('#rpg-manual-update').on('click', async function() { if (!extensionSettings.enabled) { // console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.'); @@ -734,6 +792,14 @@ async function initUI() { await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); }); + // Strip widget refresh button - same functionality as main refresh button + $('#rpg-strip-refresh').on('click', async function() { + if (!extensionSettings.enabled) { + return; + } + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + }); + $('#rpg-stat-bar-color-low').on('change', function() { extensionSettings.statBarColorLow = String($(this).val()); saveSettings(); @@ -963,6 +1029,18 @@ async function initUI() { // Toggle visibility of widget options based on master toggle $('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false); + // Initialize Desktop Strip Widget checkboxes + const stripWidgets = extensionSettings.desktopStripWidgets || {}; + $('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false); + $('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true); + $('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true); + $('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true); + $('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true); + $('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true); + $('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true); + // Toggle visibility of strip widget options based on master toggle + $('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false); + $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); $('#rpg-theme-select').val(extensionSettings.theme); @@ -1106,8 +1184,9 @@ jQuery(async () => { // Load chat-specific data for current chat try { loadChatData(); - // Initialize FAB widgets with any loaded data + // Initialize FAB widgets and strip widgets with any loaded data updateFabWidgets(); + updateStripWidgets(); } catch (error) { console.error('[RPG Companion] Chat data load failed, using defaults:', error); } diff --git a/src/core/state.js b/src/core/state.js index b3244ae..178d213 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -84,6 +84,16 @@ export let extensionSettings = { stats: { enabled: true, position: 5 }, // All stats as compact numbers attributes: { enabled: true, position: 6 } // Compact RPG attributes display }, + // Desktop strip widget display options (shown in collapsed panel strip) + desktopStripWidgets: { + enabled: false, // Master toggle for strip widgets (disabled by default) + weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.) + clock: { enabled: true }, // Current time display + date: { enabled: true }, // Date display + location: { enabled: true }, // Location name + stats: { enabled: true }, // All stats as compact numbers + attributes: { enabled: true } // Compact RPG attributes display + }, userStats: JSON.stringify({ stats: [ { id: 'health', name: 'Health', value: 100 }, diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index b5fe503..6cb3024 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -35,6 +35,7 @@ import { renderMusicPlayer } from '../rendering/musicPlayer.js'; import { i18n } from '../../core/i18n.js'; import { generateAvatarsForCharacters } from '../features/avatarGenerator.js'; import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js'; +import { updateStripWidgets } from '../ui/desktop.js'; // Store the original preset name to restore after tracker generation let originalPresetName = null; @@ -240,8 +241,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough // Update button to show "Updating..." state const $updateBtn = $('#rpg-manual-update'); + const $stripRefreshBtn = $('#rpg-strip-refresh'); const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...'; $updateBtn.html(` ${updatingText}`).prop('disabled', true); + $stripRefreshBtn.html('').prop('disabled', true); const prompt = await generateSeparateUpdatePrompt(); @@ -380,11 +383,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough setIsGenerating(false); setFabLoadingState(false); // Stop spinning FAB on mobile updateFabWidgets(); // Update FAB widgets with new data + updateStripWidgets(); // Update strip widgets with new data // Restore button to original state const $updateBtn = $('#rpg-manual-update'); + const $stripRefreshBtn = $('#rpg-strip-refresh'); const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info'; $updateBtn.html(` ${refreshText}`).prop('disabled', false); + $stripRefreshBtn.html('').prop('disabled', false); // Reset the flag after tracker generation completes // This ensures the flag persists through both main generation AND tracker generation diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index f585d42..4756145 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -45,6 +45,7 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js'; // UI import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js'; +import { updateStripWidgets } from '../ui/desktop.js'; // Chapter checkpoint import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js'; @@ -232,8 +233,9 @@ export async function onMessageReceived(data) { renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); - // Update FAB widgets with newly parsed data + // Update FAB widgets and strip widgets with newly parsed data updateFabWidgets(); + updateStripWidgets(); // Then update the DOM to reflect the cleaned message // Using updateMessageBlock to perform macro substitutions + regex formatting @@ -266,9 +268,10 @@ export async function onMessageReceived(data) { if (extensionSettings.autoUpdate && isAwaitingNewMessage) { setTimeout(async () => { await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); - // Update FAB widgets after separate/external mode update completes + // Update FAB widgets and strip widgets after separate/external mode update completes setFabLoadingState(false); updateFabWidgets(); + updateStripWidgets(); }, 500); } } @@ -294,6 +297,7 @@ export async function onMessageReceived(data) { // Stop FAB loading state and update widgets setFabLoadingState(false); updateFabWidgets(); + updateStripWidgets(); // Re-apply checkpoint in case SillyTavern unhid messages during generation await restoreCheckpointOnLoad(); @@ -332,8 +336,9 @@ export function onCharacterChanged() { renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); - // Update FAB widgets with loaded data + // Update FAB widgets and strip widgets with loaded data updateFabWidgets(); + updateStripWidgets(); // Update chat thought overlays updateChatThoughts(); @@ -501,8 +506,9 @@ export function onMessageDeleted(messageIndex) { renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); - // Update FAB widgets + // Update FAB widgets and strip widgets updateFabWidgets(); + updateStripWidgets(); // Update chat thought overlays (removes any remaining) updateChatThoughts(); @@ -555,8 +561,9 @@ export function onMessageDeleted(messageIndex) { renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); - // Update FAB widgets + // Update FAB widgets and strip widgets updateFabWidgets(); + updateStripWidgets(); // Update chat thought overlays updateChatThoughts(); @@ -591,8 +598,9 @@ export function onMessageDeleted(messageIndex) { renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); - // Update FAB widgets + // Update FAB widgets and strip widgets updateFabWidgets(); + updateStripWidgets(); // Update chat thought overlays updateChatThoughts(); diff --git a/src/systems/ui/desktop.js b/src/systems/ui/desktop.js index 40b3c20..3324c29 100644 --- a/src/systems/ui/desktop.js +++ b/src/systems/ui/desktop.js @@ -1,10 +1,273 @@ /** * Desktop UI Module - * Handles desktop-specific UI functionality: tab navigation + * Handles desktop-specific UI functionality: tab navigation and strip widgets */ import { i18n } from '../../core/i18n.js'; -import { extensionSettings } from '../../core/state.js'; +import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; + +/** + * Helper to parse time string and calculate clock hand angles + */ +function parseTimeForClock(timeStr) { + const timeMatch = timeStr.match(/(\d+):(\d+)/); + if (timeMatch) { + const hours = parseInt(timeMatch[1]); + const minutes = parseInt(timeMatch[2]); + const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute + const minuteAngle = minutes * 6; // 6° per minute + return { hourAngle, minuteAngle }; + } + return { hourAngle: 0, minuteAngle: 0 }; +} + +/** + * Updates the desktop strip widgets display based on current tracker data and settings. + * Strip widgets are shown vertically in the collapsed panel strip. + */ +export function updateStripWidgets() { + const $panel = $('#rpg-companion-panel'); + const $container = $('#rpg-strip-widget-container'); + + if ($panel.length === 0 || $container.length === 0) return; + + // Check if strip widgets are enabled + const widgetSettings = extensionSettings.desktopStripWidgets; + if (!widgetSettings || !widgetSettings.enabled) { + $panel.removeClass('rpg-strip-widgets-enabled'); + $container.find('.rpg-strip-widget').removeClass('rpg-strip-widget-visible'); + return; + } + + // Add enabled class to panel for CSS styling (wider collapsed width) + $panel.addClass('rpg-strip-widgets-enabled'); + + // Get tracker data - use imported state directly + const infoBox = lastGeneratedData?.infoBox || committedTrackerData?.infoBox; + + // Parse infoBox if it's a string + let infoData = null; + if (infoBox) { + try { + infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox; + } catch (e) { + console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e); + } + } + + // Weather Icon Widget (with description) + const $weatherWidget = $container.find('.rpg-strip-widget-weather'); + if (widgetSettings.weatherIcon?.enabled && infoData?.weather?.emoji) { + $weatherWidget.find('.rpg-strip-widget-icon').text(infoData.weather.emoji); + // Show weather description truncated + const forecast = infoData.weather.forecast || ''; + const displayForecast = forecast.length > 12 ? forecast.substring(0, 10) + '…' : forecast; + $weatherWidget.find('.rpg-strip-widget-desc').text(displayForecast); + $weatherWidget.attr('title', forecast || 'Weather'); + $weatherWidget.addClass('rpg-strip-widget-visible'); + } else { + $weatherWidget.removeClass('rpg-strip-widget-visible'); + } + + // Clock Widget with animated face + const $clockWidget = $container.find('.rpg-strip-widget-clock'); + if (widgetSettings.clock?.enabled && infoData?.time) { + const timeStr = infoData.time.end || infoData.time.value || infoData.time.start || ''; + if (timeStr) { + // Update clock hands + const { hourAngle, minuteAngle } = parseTimeForClock(timeStr); + $clockWidget.find('.rpg-strip-clock-hour').css('transform', `rotate(${hourAngle}deg)`); + $clockWidget.find('.rpg-strip-clock-minute').css('transform', `rotate(${minuteAngle}deg)`); + $clockWidget.find('.rpg-strip-widget-value').text(timeStr); + $clockWidget.attr('title', `Time: ${timeStr}`); + $clockWidget.addClass('rpg-strip-widget-visible'); + } else { + $clockWidget.removeClass('rpg-strip-widget-visible'); + } + } else { + $clockWidget.removeClass('rpg-strip-widget-visible'); + } + + // Date Widget + const $dateWidget = $container.find('.rpg-strip-widget-date'); + if (widgetSettings.date?.enabled && infoData?.date?.value) { + const dateVal = infoData.date.value; + // Truncate long dates for display + const displayDate = dateVal.length > 20 ? dateVal.substring(0, 18) + '…' : dateVal; + $dateWidget.find('.rpg-strip-widget-value').text(displayDate); + $dateWidget.attr('title', dateVal); + $dateWidget.addClass('rpg-strip-widget-visible'); + } else { + $dateWidget.removeClass('rpg-strip-widget-visible'); + } + + // Location Widget + const $locationWidget = $container.find('.rpg-strip-widget-location'); + if (widgetSettings.location?.enabled && infoData?.location?.value) { + const loc = infoData.location.value; + // Truncate long locations for display + const displayLoc = loc.length > 15 ? loc.substring(0, 13) + '…' : loc; + $locationWidget.find('.rpg-strip-widget-value').text(displayLoc); + $locationWidget.attr('title', loc); + $locationWidget.addClass('rpg-strip-widget-visible'); + } else { + $locationWidget.removeClass('rpg-strip-widget-visible'); + } + + // Stats Widget - get from lastGeneratedData or committedTrackerData first, fallback to extensionSettings + const $statsWidget = $container.find('.rpg-strip-widget-stats'); + if (widgetSettings.stats?.enabled) { + let allStats = []; + + // Try to get stats from tracker data first (most current) + const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats; + if (userStatsData) { + try { + const parsedStats = typeof userStatsData === 'string' ? JSON.parse(userStatsData) : userStatsData; + if (parsedStats?.stats) { + allStats = parsedStats.stats; + } + } catch (e) { + console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e); + } + } + + // Fallback to extensionSettings.userStats + if (allStats.length === 0 && extensionSettings.userStats) { + try { + const userStatsJson = extensionSettings.userStats; + const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson; + if (parsedUserStats?.stats) { + allStats = parsedUserStats.stats; + } + } catch (e) { + console.warn('[RPG Strip Widgets] Failed to parse extensionSettings.userStats:', e); + } + } + + if (allStats.length > 0) { + // Get enabled stats from trackerConfig + const configuredStats = extensionSettings.trackerConfig?.userStats?.customStats || []; + const enabledStatMap = new Map(); + configuredStats.forEach(s => { + if (s.enabled !== false) { + enabledStatMap.set(s.id?.toLowerCase(), true); + enabledStatMap.set(s.name?.toLowerCase(), true); + } + }); + + const $statsList = $statsWidget.find('.rpg-strip-stats-list'); + $statsList.empty(); + + allStats.forEach(stat => { + // Filter by config if available - but if no config, show all + if (configuredStats.length > 0) { + const statId = stat.id?.toLowerCase(); + const statName = stat.name?.toLowerCase(); + if (!enabledStatMap.has(statId) && !enabledStatMap.has(statName)) return; + } + + const value = typeof stat.value === 'number' ? stat.value : parseInt(stat.value) || 0; + const color = getStatColor(value); + const abbr = stat.name.substring(0, 3).toUpperCase(); + + const $item = $(`