diff --git a/index.js b/index.js index b5ae314..9c3f2de 100644 --- a/index.js +++ b/index.js @@ -328,34 +328,12 @@ async function initUI() { toggleAnimations(); }); - // Bind to both desktop and mobile refresh buttons - $('#rpg-manual-update, #rpg-manual-update-mobile').on('click', async function() { - // Get mobile button reference - const $mobileBtn = $('#rpg-manual-update-mobile'); - - // Skip if we just finished dragging the mobile button - if ($mobileBtn.data('just-dragged')) { - console.log('[RPG Companion] Click blocked - just finished dragging refresh button'); - return; - } - + $('#rpg-manual-update').on('click', async function() { if (!extensionSettings.enabled) { // console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.'); return; } - - // Remove focus to prevent sticky black state on mobile - $(this).blur(); - - // Add spinning animation to mobile button - $mobileBtn.addClass('spinning'); - - try { - await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); - } finally { - // Remove spinning animation when done - $mobileBtn.removeClass('spinning'); - } + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); }); // Reset FAB positions button diff --git a/manifest.json b/manifest.json index 0ed9fec..19e8b1f 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marysia", - "version": "1.0.0", + "version": "1.1.0", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/src/systems/rendering/infoBox.js b/src/systems/rendering/infoBox.js index 5cf3276..6aebea6 100644 --- a/src/systems/rendering/infoBox.js +++ b/src/systems/rendering/infoBox.js @@ -63,82 +63,146 @@ export function renderInfoBox() { characters: [] }; + // Track which fields we've already parsed to avoid duplicates from mixed formats + const parsedFields = { + date: false, + temperature: false, + time: false, + location: false, + weather: false + }; + for (const line of lines) { // console.log('[RPG Companion] Processing line:', line); // Support both new text format (Date:) and legacy emoji format (πŸ—“οΈ:) - if (line.startsWith('Date:') || line.includes('πŸ—“οΈ:')) { - // console.log('[RPG Companion] β†’ Matched DATE'); - const dateStr = line.replace('Date:', '').replace('πŸ—“οΈ:', '').trim(); - // Parse format: "Weekday, Month Day, Year" or "Weekday, Month, Year" - const dateParts = dateStr.split(',').map(p => p.trim()); - data.weekday = dateParts[0] || ''; - data.month = dateParts[1] || ''; - data.year = dateParts[2] || ''; - data.date = dateStr; - } else if (line.startsWith('Temperature:') || line.includes('🌑️:')) { - // console.log('[RPG Companion] β†’ Matched TEMPERATURE'); - const tempStr = line.replace('Temperature:', '').replace('🌑️:', '').trim(); - data.temperature = tempStr; - // Extract numeric value - const tempMatch = tempStr.match(/(-?\d+)/); - if (tempMatch) { - data.tempValue = parseInt(tempMatch[1]); + // Prioritize text format over emoji format + if (line.startsWith('Date:')) { + if (!parsedFields.date) { + // console.log('[RPG Companion] β†’ Matched DATE (text format)'); + const dateStr = line.replace('Date:', '').trim(); + const dateParts = dateStr.split(',').map(p => p.trim()); + data.weekday = dateParts[0] || ''; + data.month = dateParts[1] || ''; + data.year = dateParts[2] || ''; + data.date = dateStr; + parsedFields.date = true; + } + } else if (line.includes('πŸ—“οΈ:')) { + if (!parsedFields.date) { + // console.log('[RPG Companion] β†’ Matched DATE (emoji format)'); + const dateStr = line.replace('πŸ—“οΈ:', '').trim(); + const dateParts = dateStr.split(',').map(p => p.trim()); + data.weekday = dateParts[0] || ''; + data.month = dateParts[1] || ''; + data.year = dateParts[2] || ''; + data.date = dateStr; + parsedFields.date = true; + } + } else if (line.startsWith('Temperature:')) { + if (!parsedFields.temperature) { + // console.log('[RPG Companion] β†’ Matched TEMPERATURE (text format)'); + const tempStr = line.replace('Temperature:', '').trim(); + data.temperature = tempStr; + const tempMatch = tempStr.match(/(-?\d+)/); + if (tempMatch) { + data.tempValue = parseInt(tempMatch[1]); + } + parsedFields.temperature = true; + } + } else if (line.includes('🌑️:')) { + if (!parsedFields.temperature) { + // console.log('[RPG Companion] β†’ Matched TEMPERATURE (emoji format)'); + const tempStr = line.replace('🌑️:', '').trim(); + data.temperature = tempStr; + const tempMatch = tempStr.match(/(-?\d+)/); + if (tempMatch) { + data.tempValue = parseInt(tempMatch[1]); + } + parsedFields.temperature = true; + } + } else if (line.startsWith('Time:')) { + if (!parsedFields.time) { + // console.log('[RPG Companion] β†’ Matched TIME (text format)'); + const timeStr = line.replace('Time:', '').trim(); + data.time = timeStr; + const timeParts = timeStr.split('β†’').map(t => t.trim()); + data.timeStart = timeParts[0] || ''; + data.timeEnd = timeParts[1] || ''; + parsedFields.time = true; + } + } else if (line.includes('πŸ•’:')) { + if (!parsedFields.time) { + // console.log('[RPG Companion] β†’ Matched TIME (emoji format)'); + const timeStr = line.replace('πŸ•’:', '').trim(); + data.time = timeStr; + const timeParts = timeStr.split('β†’').map(t => t.trim()); + data.timeStart = timeParts[0] || ''; + data.timeEnd = timeParts[1] || ''; + parsedFields.time = true; + } + } else if (line.startsWith('Location:')) { + if (!parsedFields.location) { + // console.log('[RPG Companion] β†’ Matched LOCATION (text format)'); + data.location = line.replace('Location:', '').trim(); + parsedFields.location = true; + } + } else if (line.includes('πŸ—ΊοΈ:')) { + if (!parsedFields.location) { + // console.log('[RPG Companion] β†’ Matched LOCATION (emoji format)'); + data.location = line.replace('πŸ—ΊοΈ:', '').trim(); + parsedFields.location = true; } - } else if (line.startsWith('Time:') || line.includes('πŸ•’:')) { - // console.log('[RPG Companion] β†’ Matched TIME'); - const timeStr = line.replace('Time:', '').replace('πŸ•’:', '').trim(); - data.time = timeStr; - // Parse "HH:MM β†’ HH:MM" format - const timeParts = timeStr.split('β†’').map(t => t.trim()); - data.timeStart = timeParts[0] || ''; - data.timeEnd = timeParts[1] || ''; - } else if (line.startsWith('Location:') || line.includes('πŸ—ΊοΈ:')) { - // console.log('[RPG Companion] β†’ Matched LOCATION'); - data.location = line.replace('Location:', '').replace('πŸ—ΊοΈ:', '').trim(); } else if (line.startsWith('Weather:')) { - // New text format: Weather: [Emoji], [Forecast] - const weatherStr = line.replace('Weather:', '').trim(); - const weatherParts = weatherStr.split(',').map(p => p.trim()); - data.weatherEmoji = weatherParts[0] || ''; - data.weatherForecast = weatherParts[1] || ''; + if (!parsedFields.weather) { + // New text format: Weather: [Emoji], [Forecast] + const weatherStr = line.replace('Weather:', '').trim(); + const weatherParts = weatherStr.split(',').map(p => p.trim()); + data.weatherEmoji = weatherParts[0] || ''; + data.weatherForecast = weatherParts[1] || ''; + parsedFields.weather = true; + } } else { - // Check if it's a weather line - // Since \p{Emoji} doesn't work reliably, use a simpler approach - const hasColon = line.includes(':'); - const notInfoBox = !line.includes('Info Box'); - const notDivider = !line.includes('---'); - const notCodeFence = !line.trim().startsWith('```'); + // Check if it's a legacy weather line (emoji format) + // Only parse if we haven't already found weather in text format + if (!parsedFields.weather) { + // Since \p{Emoji} doesn't work reliably, use a simpler approach + const hasColon = line.includes(':'); + const notInfoBox = !line.includes('Info Box'); + const notDivider = !line.includes('---'); + const notCodeFence = !line.trim().startsWith('```'); - // console.log('[RPG Companion] β†’ Checking weather conditions:', { - // line: line, - // hasColon: hasColon, - // notInfoBox: notInfoBox, - // notDivider: notDivider - // }); + // console.log('[RPG Companion] β†’ Checking weather conditions:', { + // line: line, + // hasColon: hasColon, + // notInfoBox: notInfoBox, + // notDivider: notDivider + // }); - if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) { - // Match format: [Weather Emoji]: [Forecast] - // Capture everything before colon as emoji, everything after as forecast - // console.log('[RPG Companion] β†’ Testing WEATHER match for:', line); - const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/); - if (weatherMatch) { - const potentialEmoji = weatherMatch[1].trim(); - const forecast = weatherMatch[2].trim(); + if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) { + // Match format: [Weather Emoji]: [Forecast] + // Capture everything before colon as emoji, everything after as forecast + // console.log('[RPG Companion] β†’ Testing WEATHER match for:', line); + const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/); + if (weatherMatch) { + const potentialEmoji = weatherMatch[1].trim(); + const forecast = weatherMatch[2].trim(); - // If the first part is short (likely emoji), treat as weather - if (potentialEmoji.length <= 5) { - data.weatherEmoji = potentialEmoji; - data.weatherForecast = forecast; - // console.log('[RPG Companion] βœ“ Weather parsed:', data.weatherEmoji, data.weatherForecast); + // If the first part is short (likely emoji), treat as weather + if (potentialEmoji.length <= 5) { + data.weatherEmoji = potentialEmoji; + data.weatherForecast = forecast; + parsedFields.weather = true; + // console.log('[RPG Companion] βœ“ Weather parsed:', data.weatherEmoji, data.weatherForecast); + } else { + // console.log('[RPG Companion] βœ— First part too long for emoji:', potentialEmoji); + } } else { - // console.log('[RPG Companion] βœ— First part too long for emoji:', potentialEmoji); + // console.log('[RPG Companion] βœ— Weather regex did not match'); } } else { - // console.log('[RPG Companion] βœ— Weather regex did not match'); + // console.log('[RPG Companion] β†’ No match for this line'); } - } else { - // console.log('[RPG Companion] β†’ No match for this line'); } } } @@ -157,14 +221,15 @@ export function renderInfoBox() { let html = '
'; // Calendar widget - always show (editable even if empty) + // Display abbreviated version but allow editing full value const monthShort = data.month ? data.month.substring(0, 3).toUpperCase() : 'MON'; const weekdayShort = data.weekday ? data.weekday.substring(0, 3).toUpperCase() : 'DAY'; const yearDisplay = data.year || 'YEAR'; html += `
-
${monthShort}
-
${weekdayShort}
-
${yearDisplay}
+
${monthShort}
+
${weekdayShort}
+
${yearDisplay}
`; @@ -196,7 +261,8 @@ export function renderInfoBox() { `; // Time widget - always show (editable even if empty) - const timeDisplay = data.timeStart || '12:00'; + // Display the end time (second time in range) if available, otherwise start time + const timeDisplay = data.timeEnd || data.timeStart || '12:00'; // Parse time for clock hands const timeMatch = timeDisplay.match(/(\d+):(\d+)/); let hourAngle = 0; @@ -239,11 +305,32 @@ export function renderInfoBox() { // Add event handlers for editable Info Box fields $infoBoxContainer.find('.rpg-editable').on('blur', function() { - const field = $(this).data('field'); - const value = $(this).text().trim(); + const $this = $(this); + const field = $this.data('field'); + const value = $this.text().trim(); + + // For date fields, update the data-full-value immediately + if (field === 'month' || field === 'weekday' || field === 'year') { + $this.data('full-value', value); + // Update the display to show abbreviated version + if (field === 'month' || field === 'weekday') { + $this.text(value.substring(0, 3).toUpperCase()); + } else { + $this.text(value); + } + } + updateInfoBoxField(field, value); }); + // For date fields, show full value on focus + $infoBoxContainer.find('[data-field="month"], [data-field="weekday"], [data-field="year"]').on('focus', function() { + const fullValue = $(this).data('full-value'); + if (fullValue) { + $(this).text(fullValue); + } + }); + // Remove updating class after animation if (extensionSettings.enableAnimations) { setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500); @@ -512,5 +599,10 @@ export function updateInfoBoxField(field, value) { } saveChatData(); - renderInfoBox(); + + // Only re-render if NOT editing date fields + // Date fields will update on next tracker generation to avoid losing user input + if (field !== 'month' && field !== 'weekday' && field !== 'year') { + renderInfoBox(); + } } diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index b88b5f0..9993154 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -34,9 +34,6 @@ export function closeMobilePanelWithAnimation() { $panel.removeClass('rpg-mobile-open').addClass('rpg-mobile-closing'); $mobileToggle.removeClass('active'); - // Trigger event for other components (like refresh button) - $(document).trigger('rpg-panel-toggled', { isOpen: false }); - // Wait for animation to complete before hiding $panel.one('animationend', function() { $panel.removeClass('rpg-mobile-closing'); @@ -130,9 +127,6 @@ export function setupCollapseToggle() { const $overlay = $('
'); $('body').append($overlay); - // Trigger event for other components (like refresh button) - $(document).trigger('rpg-panel-toggled', { isOpen: true }); - // Debug: Check state after animation should complete setTimeout(() => { console.log('[RPG Mobile] 500ms after opening:', { @@ -273,13 +267,10 @@ export function applyPanelPosition() { */ export function updateGenerationModeUI() { if (extensionSettings.generationMode === 'together') { - // In "together" mode, hide both update buttons + // In "together" mode, manual update button is hidden $('#rpg-manual-update').hide(); - $('#rpg-manual-update-mobile').hide(); } else { - // In "separate" mode, show both buttons - // (CSS media queries control which one is visible based on viewport) + // In "separate" mode, manual update button is visible $('#rpg-manual-update').show(); - $('#rpg-manual-update-mobile').show(); } } diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index d29d758..73a4ff1 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -278,9 +278,6 @@ export function setupMobileToggle() { $('body').append($overlay); $mobileToggle.addClass('active'); - // Trigger event for other components (like refresh button) - $(document).trigger('rpg-panel-toggled', { isOpen: true }); - // Close when clicking overlay $overlay.on('click', function() { closeMobilePanelWithAnimation(); @@ -313,9 +310,6 @@ export function setupMobileToggle() { $('body').append($overlay); $mobileToggle.addClass('active'); - // Trigger event for other components (like refresh button) - $(document).trigger('rpg-panel-toggled', { isOpen: true }); - $overlay.on('click', function() { console.log('[RPG Mobile] Overlay clicked - closing panel'); closeMobilePanelWithAnimation(); @@ -440,41 +434,32 @@ export function setupMobileToggle() { * Constrains the mobile FAB button to viewport bounds with top-bar awareness. * Only runs when button is in user-controlled state (mobileFabPosition exists). * Ensures button never goes behind the top bar or outside viewport edges. - * @param {jQuery} $button - Optional button element (defaults to mobile toggle) */ -export function constrainFabToViewport($button = null) { - // Default to mobile toggle if no button specified - if (!$button) { - $button = $('#rpg-mobile-toggle'); - } - - if ($button.length === 0) return; - - // Determine which position setting to check based on button ID - const isRefreshButton = $button.attr('id') === 'rpg-manual-update-mobile'; - const positionSetting = isRefreshButton ? 'mobileRefreshPosition' : 'mobileFabPosition'; - +export function constrainFabToViewport() { // Only constrain if user has set a custom position - if (!extensionSettings[positionSetting]) { + if (!extensionSettings.mobileFabPosition) { console.log('[RPG Mobile] Skipping viewport constraint - using CSS defaults'); return; } + const $mobileToggle = $('#rpg-mobile-toggle'); + if ($mobileToggle.length === 0) return; + // Skip if button is not visible - if (!$button.is(':visible')) { + if (!$mobileToggle.is(':visible')) { console.log('[RPG Mobile] Skipping viewport constraint - button not visible'); return; } // Get current position - const offset = $button.offset(); + const offset = $mobileToggle.offset(); if (!offset) return; let currentX = offset.left; let currentY = offset.top; - const buttonWidth = $button.outerWidth(); - const buttonHeight = $button.outerHeight(); + const buttonWidth = $mobileToggle.outerWidth(); + const buttonHeight = $mobileToggle.outerHeight(); // Get top bar height from CSS variable (fallback to 50px if not set) const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--topBarBlockSize')) || 50; @@ -500,15 +485,15 @@ export function constrainFabToViewport($button = null) { }); // Apply new position - $button.css({ + $mobileToggle.css({ left: newX + 'px', top: newY + 'px', right: 'auto', bottom: 'auto' }); - // Save corrected position to appropriate setting - extensionSettings[positionSetting] = { + // Save corrected position + extensionSettings.mobileFabPosition = { left: newX + 'px', top: newY + 'px' }; diff --git a/style.css b/style.css index a03ec60..e69ae64 100644 --- a/style.css +++ b/style.css @@ -576,8 +576,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-user-portrait { - width: clamp(24px, 4vh, 32px); - height: clamp(24px, 4vh, 32px); + width: clamp(1.7vw, 1.8vw, 1.9vw); + height: clamp(1.7vw, 1.8vw, 1.9vw); border-radius: 50%; border: 2px solid var(--rpg-highlight); box-shadow: 0 0 8px var(--rpg-highlight); @@ -726,7 +726,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* User name and level - inline with portrait */ .rpg-user-name { font-weight: 600; - font-size: 1em; + font-size: 0.7vw; color: var(--rpg-text-color); white-space: nowrap; overflow: hidden; @@ -734,17 +734,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-level-label { - font-size: 1em; + font-size: 0.7vw; font-weight: 600; color: var(--rpg-text-color); opacity: 0.7; } .rpg-level-value { - font-size: 1em; + font-size: 0.7vw; font-weight: 700; color: var(--rpg-highlight-color); - padding: 0 0.375em; + padding: clamp(1px, 0.2vh, 2px) 0.375em; background: var(--rpg-accent-color); border-radius: clamp(2px, 0.3vh, 3px); border: 1px solid var(--rpg-highlight-color); @@ -2698,7 +2698,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } /* ============================================ - MANUAL UPDATE BUTTON (Desktop) + MANUAL UPDATE BUTTON ============================================ */ .rpg-manual-update-btn { width: 100%; @@ -2735,64 +2735,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); } -/* ============================================ - MOBILE REFRESH BUTTON (FAB - Same pattern as toggle) - ============================================ */ -.rpg-mobile-refresh { - display: none; - align-items: center; - justify-content: center; - position: fixed; - /* Position set by JavaScript based on saved settings */ - width: 44px; - height: 44px; - border-radius: 50%; - background: var(--SmartThemeBlurTintColor); - border: 2px solid var(--SmartThemeBorderColor); - color: var(--rpg-text, #ecf0f1); - font-size: 1.85vw; - cursor: grab; - z-index: 1001; /* Above panel (1000) but below mobile toggle (10002) */ - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; - user-select: none; - -webkit-user-select: none; - will-change: top, left; -} - -/* Disable transitions while actively dragging */ -.rpg-mobile-refresh.dragging { - transition: none; - cursor: grabbing; -} - -.rpg-mobile-refresh:hover { - transform: scale(1.1); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); -} - -.rpg-mobile-refresh:active { - transform: scale(0.95); -} - -/* Spinning animation when refreshing */ -.rpg-mobile-refresh.spinning i { - animation: rpg-spin 0.8s linear infinite; -} - -.rpg-mobile-refresh i { - pointer-events: none; -} - -@keyframes rpg-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - /* ============================================ SETTINGS BUTTON ============================================ */ @@ -3378,31 +3320,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Mobile-specific panel behavior - matches SillyTavern's 1000px breakpoint */ /* CACHE BUST v2025-01-16 */ -/* Mobile refresh button visibility - opposite of toggle */ -@media (max-width: 1000px) { - /* Show mobile refresh button */ - .rpg-mobile-refresh { - display: flex; - } - - /* Hide desktop refresh button */ - .rpg-manual-update-btn { - display: none !important; - } - - /* Show refresh button when panel is open (opposite of toggle) */ - body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-refresh { - opacity: 1; - pointer-events: auto; - } - - /* Hide refresh button when panel is closed */ - .rpg-mobile-refresh { - opacity: 0; - pointer-events: none; - } -} - @media (max-width: 1000px) { /* ======================================== MOBILE PANEL FOUNDATION @@ -3727,9 +3644,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-calendar-day { font-size: clamp(11px, 2.9vw, 14px) !important; - min-height: 3em !important; /* Ensure enough height for content to center */ - padding: 0.75em 0.5em !important; /* More vertical padding on mobile */ - line-height: 1.2 !important; /* Tighter line height */ } .rpg-calendar-year { @@ -3867,20 +3781,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld { grid-row: 3; display: flex; flex-direction: column; - gap: 3px !important; /* Reduced from 6px for more compact display */ + gap: 6px; min-width: 0; - padding: 4px 0.375em !important; /* Reduced vertical padding */ } /* Make mood text readable on mobile */ .rpg-mood-conditions { font-size: clamp(11px, 2.8vw, 14px); - line-height: 1.2 !important; /* Tighter line height */ - } - - /* Smaller emoji on mobile */ - .rpg-mood-emoji { - font-size: clamp(14px, 3.5vw, 18px) !important; /* Slightly smaller */ + line-height: 1.3; } /* Attributes - right side, rows 4-6 aligned with mood */ @@ -4080,11 +3988,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: clamp(20px, 5.1vw, 26px) !important; } - /* Larger mobile refresh icon (same as toggle) */ - .rpg-mobile-refresh { - font-size: clamp(20px, 5.1vw, 26px) !important; - } - /* ======================================== MOBILE SETTINGS POPUP ======================================== */ @@ -4188,13 +4091,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld { min-height: 2.75rem; } - /* Exception: Level value should stay compact */ - .rpg-level-value.rpg-editable { - padding: 0 0.375em; - min-height: auto; - line-height: 1.2; - } - /* Larger close buttons */ .rpg-thought-close { min-width: 2.75rem;