Add holiday promotion, snowflakes effect, and Spotify music widget
- Added holiday promotion banner with 2026WITHMARINARA discount code - Added dismiss functionality for promotion with persistent state - Implemented snowflakes animation effect with toggle - Added Spotify music widget above chat input - Widget matches extension theme colors and positioning - Added Display Options toggles to show/hide feature toggles - Improved responsive design and mobile support
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
$thoughtsContainer,
|
||||
$inventoryContainer,
|
||||
$questsContainer,
|
||||
$musicPlayerContainer,
|
||||
setExtensionSettings,
|
||||
updateExtensionSettings,
|
||||
setLastGeneratedData,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
setThoughtsContainer,
|
||||
setInventoryContainer,
|
||||
setQuestsContainer,
|
||||
setMusicPlayerContainer,
|
||||
clearSessionAvatarPrompts
|
||||
} from './src/core/state.js';
|
||||
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
|
||||
@@ -66,6 +68,8 @@ import {
|
||||
} from './src/systems/rendering/thoughts.js';
|
||||
import { renderInventory } from './src/systems/rendering/inventory.js';
|
||||
import { renderQuests } from './src/systems/rendering/quests.js';
|
||||
import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js';
|
||||
import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js';
|
||||
|
||||
// Interaction modules
|
||||
import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js';
|
||||
@@ -76,6 +80,7 @@ import {
|
||||
applyCustomTheme,
|
||||
toggleCustomColors,
|
||||
toggleAnimations,
|
||||
updateFeatureTogglesVisibility,
|
||||
updateSettingsPopupTheme,
|
||||
applyCustomThemeToSettingsPopup
|
||||
} from './src/systems/ui/theme.js';
|
||||
@@ -130,6 +135,7 @@ import { setupClassicStatsButtons } from './src/systems/features/classicStats.js
|
||||
import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js';
|
||||
import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
|
||||
import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
|
||||
import { parseAndStoreSpotifyUrl } from './src/systems/features/musicPlayer.js';
|
||||
import { DEFAULT_HTML_PROMPT } from './src/systems/generation/promptBuilder.js';
|
||||
import { openEncounterModal } from './src/systems/ui/encounterUI.js';
|
||||
|
||||
@@ -277,6 +283,7 @@ async function initUI() {
|
||||
setThoughtsContainer($('#rpg-thoughts'));
|
||||
setInventoryContainer($('#rpg-inventory'));
|
||||
setQuestsContainer($('#rpg-quests'));
|
||||
setMusicPlayerContainer($('#rpg-music-player'));
|
||||
|
||||
// Re-apply translations to the entire body to catch all new elements from the template
|
||||
i18n.applyTranslations(document.body);
|
||||
@@ -377,6 +384,25 @@ async function initUI() {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-spotify-music').on('change', function() {
|
||||
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateSectionVisibility();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
});
|
||||
|
||||
$('#rpg-toggle-snowflakes').on('change', function() {
|
||||
extensionSettings.enableSnowflakes = $(this).prop('checked');
|
||||
saveSettings();
|
||||
toggleSnowflakes(extensionSettings.enableSnowflakes);
|
||||
});
|
||||
|
||||
$('#rpg-dismiss-promo').on('click', function() {
|
||||
extensionSettings.dismissedHolidayPromo = true;
|
||||
saveSettings();
|
||||
$('#rpg-holiday-promo').fadeOut(300);
|
||||
});
|
||||
|
||||
$('#rpg-skip-guided-mode').on('change', function() {
|
||||
extensionSettings.skipInjectionsForGuided = String($(this).val());
|
||||
saveSettings();
|
||||
@@ -516,6 +542,25 @@ async function initUI() {
|
||||
toggleAnimations();
|
||||
});
|
||||
|
||||
// Feature toggle visibility controls
|
||||
$('#rpg-toggle-show-html-toggle').on('change', function() {
|
||||
extensionSettings.showHtmlToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
|
||||
extensionSettings.showSpotifyToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-show-snowflakes-toggle').on('change', function() {
|
||||
extensionSettings.showSnowflakesToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
// Auto avatar generation settings
|
||||
$('#rpg-toggle-auto-avatars').on('change', function() {
|
||||
extensionSettings.autoGenerateAvatars = $(this).prop('checked');
|
||||
@@ -710,6 +755,18 @@ async function initUI() {
|
||||
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
||||
$('#rpg-toggle-always-show-bubble').prop('checked', extensionSettings.alwaysShowThoughtBubble);
|
||||
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
||||
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
|
||||
$('#rpg-toggle-snowflakes').prop('checked', extensionSettings.enableSnowflakes);
|
||||
|
||||
// Feature toggle visibility settings
|
||||
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
|
||||
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
|
||||
$('#rpg-toggle-show-snowflakes-toggle').prop('checked', extensionSettings.showSnowflakesToggle ?? true);
|
||||
|
||||
// Hide holiday promo if previously dismissed
|
||||
if (extensionSettings.dismissedHolidayPromo) {
|
||||
$('#rpg-holiday-promo').hide();
|
||||
}
|
||||
|
||||
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
|
||||
$('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true);
|
||||
@@ -766,6 +823,7 @@ async function initUI() {
|
||||
applyPanelPosition();
|
||||
toggleCustomColors();
|
||||
toggleAnimations();
|
||||
updateFeatureTogglesVisibility();
|
||||
|
||||
// Setup mobile toggle button
|
||||
setupMobileToggle();
|
||||
@@ -784,6 +842,7 @@ async function initUI() {
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
updateDiceDisplay();
|
||||
setupDiceRoller();
|
||||
setupClassicStatsButtons();
|
||||
@@ -983,6 +1042,14 @@ jQuery(async () => {
|
||||
// Restore checkpoint state if one exists
|
||||
await restoreCheckpointOnLoad();
|
||||
|
||||
// Initialize snowflakes effect if enabled
|
||||
try {
|
||||
initSnowflakes();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Snowflakes initialization failed:', error);
|
||||
// Non-critical - continue without it
|
||||
}
|
||||
|
||||
console.log('[RPG Companion] ✅ Extension loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] ❌ Critical initialization failure:', error);
|
||||
|
||||
@@ -35,6 +35,8 @@ export const defaultSettings = {
|
||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||
alwaysShowThoughtBubble: false, // Auto-expand thought bubble without clicking icon
|
||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||
// Controls when the extension skips injecting tracker instructions/examples/HTML
|
||||
// into generations that appear to be user-injected instructions. Valid values:
|
||||
// - 'none' -> never skip (legacy behavior: always inject)
|
||||
|
||||
@@ -24,6 +24,13 @@ export let extensionSettings = {
|
||||
narratorMode: false, // Use character card as narrator instead of fixed character references
|
||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||
enableSnowflakes: false, // Enable festive snowflakes effect
|
||||
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
||||
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
||||
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
||||
showSnowflakesToggle: true, // Show Snowflakes Effect toggle in main panel
|
||||
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
||||
enablePlotButtons: true, // Show plot progression buttons above chat input
|
||||
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
||||
@@ -283,6 +290,7 @@ export let $infoBoxContainer = null;
|
||||
export let $thoughtsContainer = null;
|
||||
export let $inventoryContainer = null;
|
||||
export let $questsContainer = null;
|
||||
export let $musicPlayerContainer = null;
|
||||
|
||||
/**
|
||||
* State setters - provide controlled mutation of state variables
|
||||
@@ -354,3 +362,7 @@ export function setInventoryContainer($element) {
|
||||
export function setQuestsContainer($element) {
|
||||
$questsContainer = $element;
|
||||
}
|
||||
|
||||
export function setMusicPlayerContainer($element) {
|
||||
$musicPlayerContainer = $element;
|
||||
}
|
||||
|
||||
+9
-2
@@ -39,6 +39,9 @@
|
||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
|
||||
"template.settingsModal.display.enableAnimations": "Enable Animations",
|
||||
"template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls",
|
||||
"template.settingsModal.display.showImmersiveHtmlToggle": "Show Immersive HTML",
|
||||
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
|
||||
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
|
||||
"template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons",
|
||||
"template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts",
|
||||
"template.settingsModal.display.showDiceDisplay": "Show Dice Roll Display",
|
||||
@@ -126,7 +129,9 @@
|
||||
"template.mainPanel.title": "RPG Companion",
|
||||
"template.mainPanel.lastRoll": "Last Roll:",
|
||||
"template.mainPanel.clearLastRoll": "Clear last roll",
|
||||
"template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML",
|
||||
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
||||
"template.mainPanel.spotifyMusic": "Spotify Music",
|
||||
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
|
||||
"template.mainPanel.refreshRpgInfo": "Refresh RPG Info",
|
||||
"template.mainPanel.updating": "Updating...",
|
||||
"template.mainPanel.editTrackersButton": "Edit Trackers",
|
||||
@@ -183,5 +188,7 @@
|
||||
"checkpoint.setChapterStart": "Set Chapter Start",
|
||||
"checkpoint.clearChapterStart": "Clear Chapter Start",
|
||||
"checkpoint.indicator": "Chapter Start",
|
||||
"checkpoint.tooltip": "Messages before this point are excluded from context"
|
||||
"checkpoint.tooltip": "Messages before this point are excluded from context",
|
||||
"musicPlayer.title": "Scene Music",
|
||||
"musicPlayer.noMusic": "AI will suggest music when appropriate for the scene"
|
||||
}
|
||||
|
||||
+9
-2
@@ -36,6 +36,9 @@
|
||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
|
||||
"template.settingsModal.display.enableAnimations": "啟用動畫",
|
||||
"template.settingsModal.display.enableAnimationsNote": "屬性、內容更新和擲骰的動畫效果",
|
||||
"template.settingsModal.display.showImmersiveHtmlToggle": "顯示沉浸式 HTML",
|
||||
"template.settingsModal.display.showSpotifyMusicToggle": "顯示 Spotify 音樂",
|
||||
"template.settingsModal.display.showSnowflakesToggle": "顯示雪花效果",
|
||||
"template.settingsModal.display.showPlotProgressionButtons": "顯示劇情推進按鈕(QR)",
|
||||
"template.settingsModal.display.showPlotProgressionButtonsNote": "在聊天輸入框上方顯示劇情推進提示按鈕(QR)",
|
||||
"template.settingsModal.display.enableDebugMode": "Debug Mode",
|
||||
@@ -119,7 +122,9 @@
|
||||
"template.mainPanel.title": "RPG Companion",
|
||||
"template.mainPanel.lastRoll": "上次擲骰:",
|
||||
"template.mainPanel.clearLastRoll": "清除上次擲骰",
|
||||
"template.mainPanel.enableImmersiveHtml": "啟用沉浸式 HTML",
|
||||
"template.mainPanel.immersiveHtml": "沉浸式 HTML",
|
||||
"template.mainPanel.spotifyMusic": "Spotify 音樂",
|
||||
"template.mainPanel.snowflakesEffect": "雪花效果",
|
||||
"template.mainPanel.refreshRpgInfo": "刷新資訊",
|
||||
"template.mainPanel.updating": "更新中...",
|
||||
"template.mainPanel.editTrackersButton": "追蹤器編輯",
|
||||
@@ -172,5 +177,7 @@
|
||||
"quests.optional.addQuestButton": "添加支線任務",
|
||||
"quests.optional.addQuestPlaceholder": "輸入支線任務名稱...",
|
||||
"quests.optional.empty": "當前無支線任務 (ʘ̆ʚʘ̆)",
|
||||
"quests.optional.hint": "支線任務是補充主線劇情的支線目標。"
|
||||
"quests.optional.hint": "支線任務是補充主線劇情的支線目標。",
|
||||
"musicPlayer.title": "場景音樂",
|
||||
"musicPlayer.noMusic": "AI 會在適當時為場景建議音樂"
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Music Player Module
|
||||
* Handles parsing and storing Spotify URLs from AI responses
|
||||
*/
|
||||
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
|
||||
/**
|
||||
* Extracts song suggestion from AI response in <spotify:Song - Artist/> format
|
||||
* @param {string} responseText - The raw AI response text
|
||||
* @returns {Object|null} Object with {song, artist, searchQuery} or null if not found
|
||||
*/
|
||||
export function extractSpotifyUrl(responseText) {
|
||||
if (!responseText || !extensionSettings.enableSpotifyMusic) return null;
|
||||
|
||||
// Match <spotify:Song Title - Artist Name/> format
|
||||
const songMatch = responseText.match(/<spotify:([^<>-]+)\s*-\s*([^<>\/]+)\/>/i);
|
||||
if (songMatch) {
|
||||
const song = songMatch[1].trim();
|
||||
const artist = songMatch[2].trim();
|
||||
const searchQuery = `${song} ${artist}`;
|
||||
return {
|
||||
song,
|
||||
artist,
|
||||
searchQuery,
|
||||
displayText: `${song} - ${artist}`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts song data to Spotify app protocol URL
|
||||
* @param {Object} songData - Object with {song, artist, searchQuery}
|
||||
* @returns {string} Spotify app protocol URL
|
||||
*/
|
||||
export function convertToEmbedUrl(songData) {
|
||||
if (!songData || !songData.searchQuery) return '';
|
||||
|
||||
// Use Spotify app protocol for direct app opening
|
||||
const encodedQuery = encodeURIComponent(songData.searchQuery);
|
||||
return `spotify:search:${encodedQuery}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses AI response for song suggestion and stores it
|
||||
* @param {string} responseText - The raw AI response text
|
||||
* @returns {boolean} True if song was found and stored
|
||||
*/
|
||||
export function parseAndStoreSpotifyUrl(responseText) {
|
||||
if (!extensionSettings.enableSpotifyMusic) return false;
|
||||
|
||||
const songData = extractSpotifyUrl(responseText);
|
||||
console.log('[RPG Companion] Spotify Parser: Found song:', songData);
|
||||
if (songData) {
|
||||
// Store in committed tracker data
|
||||
committedTrackerData.spotifyUrl = songData;
|
||||
console.log('[RPG Companion] Spotify Parser: Stored song in committedTrackerData:', committedTrackerData.spotifyUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current song data from committed tracker data
|
||||
* @returns {Object|null} Current song data or null
|
||||
*/
|
||||
export function getCurrentSpotifyUrl() {
|
||||
return committedTrackerData.spotifyUrl || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current song data
|
||||
*/
|
||||
export function clearSpotifyUrl() {
|
||||
committedTrackerData.spotifyUrl = null;
|
||||
}
|
||||
@@ -12,18 +12,21 @@ import {
|
||||
isGenerating,
|
||||
lastActionWasSwipe,
|
||||
setIsGenerating,
|
||||
setLastActionWasSwipe
|
||||
setLastActionWasSwipe,
|
||||
$musicPlayerContainer
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData } from '../../core/persistence.js';
|
||||
import {
|
||||
generateSeparateUpdatePrompt
|
||||
} from './promptBuilder.js';
|
||||
import { parseResponse, parseUserStats } from './parser.js';
|
||||
import { parseAndStoreSpotifyUrl } from '../features/musicPlayer.js';
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
||||
|
||||
@@ -267,6 +270,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
if (response) {
|
||||
// console.log('[RPG Companion] Raw AI response:', response);
|
||||
const parsedData = parseResponse(response);
|
||||
// Parse and store Spotify URL if feature is enabled
|
||||
parseAndStoreSpotifyUrl(response);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
|
||||
|
||||
@@ -346,6 +351,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
generateTrackerExample,
|
||||
generateTrackerInstructions,
|
||||
generateContextualSummary,
|
||||
DEFAULT_HTML_PROMPT
|
||||
DEFAULT_HTML_PROMPT,
|
||||
DEFAULT_SPOTIFY_PROMPT,
|
||||
SPOTIFY_FORMAT_INSTRUCTION
|
||||
} from './promptBuilder.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
|
||||
@@ -67,6 +69,7 @@ export async function onGenerationStarted(type, data) {
|
||||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
}
|
||||
|
||||
@@ -230,6 +233,19 @@ export async function onGenerationStarted(type, data) {
|
||||
// Clear HTML prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
|
||||
} else {
|
||||
// Clear Spotify prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
} else if (extensionSettings.generationMode === 'separate') {
|
||||
// In SEPARATE mode, inject the contextual summary for main roleplay generation
|
||||
const contextSummary = generateContextualSummary();
|
||||
@@ -266,6 +282,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate mode');
|
||||
} else {
|
||||
// Clear Spotify prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Clear together mode injections
|
||||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
@@ -274,5 +303,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,16 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
|
||||
*/
|
||||
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
|
||||
|
||||
/**
|
||||
* Default Spotify music prompt text (customizable by users)
|
||||
*/
|
||||
export const DEFAULT_SPOTIFY_PROMPT = `If appropriate for the current scene's mood and atmosphere, suggest a song that fits the ambiance. Choose music that enhances the emotional tone, setting, or action of the scene.`;
|
||||
|
||||
/**
|
||||
* Spotify format instruction (constant, not editable by users)
|
||||
*/
|
||||
export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spotify:Song Title - Artist Name/>.`;
|
||||
|
||||
/**
|
||||
* Gets character card information for current chat (handles both single and group chats)
|
||||
* @returns {string} Formatted character information
|
||||
@@ -445,6 +455,20 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += htmlPrompt;
|
||||
}
|
||||
|
||||
// Append Spotify music prompt if enabled AND includeHtmlPrompt is true
|
||||
if (extensionSettings.enableSpotifyMusic && includeHtmlPrompt) {
|
||||
// Add separator
|
||||
if (hasAnyTrackers || extensionSettings.enableHtmlPrompt) {
|
||||
instructions += `\n\n`;
|
||||
} else {
|
||||
instructions += `\n`;
|
||||
}
|
||||
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
const spotifyPrompt = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||
instructions += spotifyPrompt + ' ' + SPOTIFY_FORMAT_INSTRUCTION;
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,14 @@ import {
|
||||
setLastActionWasSwipe,
|
||||
setIsPlotProgression,
|
||||
updateLastGeneratedData,
|
||||
updateCommittedTrackerData
|
||||
updateCommittedTrackerData,
|
||||
$musicPlayerContainer
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData, loadChatData } from '../../core/persistence.js';
|
||||
|
||||
// Generation & Parsing
|
||||
import { parseResponse, parseUserStats } from '../generation/parser.js';
|
||||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||||
import { updateRPGData } from '../generation/apiClient.js';
|
||||
|
||||
// Rendering
|
||||
@@ -30,6 +32,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||
|
||||
// Utils
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
@@ -119,6 +122,8 @@ export async function onMessageReceived(data) {
|
||||
// console.log('[RPG Companion] Parsing together mode response:', responseText);
|
||||
|
||||
const parsedData = parseResponse(responseText);
|
||||
// Parse and store Spotify URL if feature is enabled
|
||||
parseAndStoreSpotifyUrl(responseText);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
|
||||
// Update stored data
|
||||
@@ -176,6 +181,7 @@ export async function onMessageReceived(data) {
|
||||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
||||
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
|
||||
|
||||
// Update the message in chat history
|
||||
lastMessage.mes = cleanedMessage.trim();
|
||||
@@ -191,6 +197,7 @@ export async function onMessageReceived(data) {
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Then update the DOM to reflect the cleaned message
|
||||
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
||||
@@ -202,12 +209,29 @@ export async function onMessageReceived(data) {
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
}
|
||||
} else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) {
|
||||
// In separate mode with auto-update, trigger update after message
|
||||
} else if (extensionSettings.generationMode === 'separate') {
|
||||
// In separate mode, also parse Spotify URLs from the main roleplay response
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
if (lastMessage && !lastMessage.is_user) {
|
||||
const responseText = lastMessage.mes;
|
||||
|
||||
// Parse and store Spotify URL
|
||||
const foundSpotifyUrl = parseAndStoreSpotifyUrl(responseText);
|
||||
|
||||
// No need to clean message - SillyTavern auto-hides <Song - Artist/> tags
|
||||
if (foundSpotifyUrl && extensionSettings.enableSpotifyMusic) {
|
||||
// Just render the music player
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger auto-update if enabled
|
||||
if (extensionSettings.autoUpdate) {
|
||||
setTimeout(async () => {
|
||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the swipe flag after generation completes
|
||||
// This ensures that if the user swiped → auto-reply generated → flag is now cleared
|
||||
@@ -253,6 +277,7 @@ export function onCharacterChanged() {
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
@@ -328,6 +353,7 @@ export function onMessageSwiped(messageIndex) {
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Music Player Rendering Module
|
||||
* Handles UI rendering for Spotify music player widget
|
||||
*/
|
||||
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
/**
|
||||
* Creates a Spotify deep link URL that opens the Spotify app
|
||||
* Uses spotify:search: protocol for app, falls back to web URL
|
||||
* @param {Object} songData - Object with {song, artist, searchQuery}
|
||||
* @returns {Object} Object with appUrl and webUrl
|
||||
*/
|
||||
function createSpotifyUrls(songData) {
|
||||
if (!songData || !songData.searchQuery) {
|
||||
return { appUrl: '', webUrl: '' };
|
||||
}
|
||||
|
||||
const encodedQuery = encodeURIComponent(songData.searchQuery);
|
||||
|
||||
return {
|
||||
// Spotify app protocol - opens directly in Spotify app on desktop/mobile
|
||||
appUrl: `spotify:search:${encodedQuery}`,
|
||||
// Web fallback - opens Spotify web player search
|
||||
webUrl: `https://open.spotify.com/search/${encodedQuery}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Spotify with the given song
|
||||
* Tries app protocol first, falls back to web
|
||||
* @param {Object} songData - Song data object
|
||||
*/
|
||||
function openInSpotify(songData) {
|
||||
const urls = createSpotifyUrls(songData);
|
||||
|
||||
// Try to open in Spotify app first
|
||||
// On mobile, this will open the Spotify app if installed
|
||||
// On desktop, this will open Spotify desktop app if installed
|
||||
window.location.href = urls.appUrl;
|
||||
|
||||
// Fallback: If app doesn't open within 2 seconds, open web version
|
||||
// This handles cases where Spotify app isn't installed
|
||||
setTimeout(() => {
|
||||
// Check if we're still on the same page (app didn't open)
|
||||
// Note: This is a best-effort fallback
|
||||
if (document.hasFocus()) {
|
||||
window.open(urls.webUrl, '_blank');
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Spotify music player as a mini player widget above chat input
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
export function renderMusicPlayer(container) {
|
||||
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);
|
||||
|
||||
if (!extensionSettings.enableSpotifyMusic) {
|
||||
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);
|
||||
|
||||
if (!songData || !songData.displayText) {
|
||||
// No song - don't show anything
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the mini music player widget
|
||||
const musicPlayerHtml = `
|
||||
<div id="rpg-chat-music-player" class="rpg-music-widget">
|
||||
<div class="rpg-music-widget-content">
|
||||
<div class="rpg-music-widget-icon">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
</div>
|
||||
<div class="rpg-music-widget-info">
|
||||
<div class="rpg-music-widget-title" title="${songData.song}">${songData.song}</div>
|
||||
<div class="rpg-music-widget-artist" title="${songData.artist}">${songData.artist}</div>
|
||||
</div>
|
||||
<button class="rpg-music-widget-play" title="Play in Spotify">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button class="rpg-music-widget-close" title="Dismiss">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Find the chat form container and insert widget before (above) it
|
||||
const $chatForm = $('#send_form');
|
||||
|
||||
console.log('[RPG Companion] Music Player: Found #send_form:', $chatForm.length > 0);
|
||||
|
||||
if ($chatForm.length === 0) {
|
||||
console.error('[RPG Companion] Music Player: Could not find #send_form - cannot render widget!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert widget inside (at top of) the chat form
|
||||
console.log('[RPG Companion] Music Player: Prepending widget to #send_form');
|
||||
$chatForm.prepend(musicPlayerHtml);
|
||||
|
||||
console.log('[RPG Companion] Music Player: Widget inserted, checking if visible...');
|
||||
const $widget = $('#rpg-chat-music-player');
|
||||
console.log('[RPG Companion] Music Player: Widget exists:', $widget.length > 0);
|
||||
if ($widget.length > 0) {
|
||||
console.log('[RPG Companion] Music Player: Widget position:', $widget.offset());
|
||||
console.log('[RPG Companion] Music Player: Widget dimensions:', { width: $widget.width(), height: $widget.height() });
|
||||
console.log('[RPG Companion] Music Player: Widget CSS display:', $widget.css('display'));
|
||||
console.log('[RPG Companion] Music Player: Widget CSS visibility:', $widget.css('visibility'));
|
||||
}
|
||||
|
||||
// Bind play button click
|
||||
$('#rpg-chat-music-player .rpg-music-widget-play').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
openInSpotify(songData);
|
||||
});
|
||||
|
||||
// Bind close button click
|
||||
$('#rpg-chat-music-player .rpg-music-widget-close').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
$('#rpg-chat-music-player').fadeOut(200, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Clicking anywhere else on the widget also opens Spotify
|
||||
$('#rpg-chat-music-player .rpg-music-widget-content').on('click', function() {
|
||||
openInSpotify(songData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the music player display
|
||||
* @param {HTMLElement} container - Container element
|
||||
*/
|
||||
export function updateMusicPlayer(container) {
|
||||
renderMusicPlayer(container);
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
$infoBoxContainer,
|
||||
$thoughtsContainer,
|
||||
$inventoryContainer,
|
||||
$questsContainer
|
||||
$questsContainer,
|
||||
$musicPlayerContainer
|
||||
} from '../../core/state.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
@@ -283,10 +284,18 @@ export function updateSectionVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
if ($musicPlayerContainer) {
|
||||
if (extensionSettings.enableSpotifyMusic) {
|
||||
$musicPlayerContainer.show();
|
||||
} else {
|
||||
$musicPlayerContainer.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide dividers intelligently
|
||||
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
|
||||
const showDividerAfterStats = extensionSettings.showUserStats &&
|
||||
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests);
|
||||
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||
if (showDividerAfterStats) {
|
||||
$('#rpg-divider-stats').show();
|
||||
} else {
|
||||
@@ -304,20 +313,28 @@ export function updateSectionVisibility() {
|
||||
|
||||
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
|
||||
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
|
||||
(extensionSettings.showInventory || extensionSettings.showQuests);
|
||||
(extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||
if (showDividerAfterThoughts) {
|
||||
$('#rpg-divider-thoughts').show();
|
||||
} else {
|
||||
$('#rpg-divider-thoughts').hide();
|
||||
}
|
||||
|
||||
// Divider after Inventory: shown if Inventory is visible AND Quests is visible
|
||||
const showDividerAfterInventory = extensionSettings.showInventory && extensionSettings.showQuests;
|
||||
// Divider after Inventory: shown if Inventory is visible AND (Quests or Music) is visible
|
||||
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||
if (showDividerAfterInventory) {
|
||||
$('#rpg-divider-inventory').show();
|
||||
} else {
|
||||
$('#rpg-divider-inventory').hide();
|
||||
}
|
||||
|
||||
// Divider after Quests: shown if Quests is visible AND Music is visible
|
||||
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
|
||||
if (showDividerAfterQuests) {
|
||||
$('#rpg-divider-quests').show();
|
||||
} else {
|
||||
$('#rpg-divider-quests').hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { DEFAULT_HTML_PROMPT } from '../generation/promptBuilder.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_SPOTIFY_PROMPT } from '../generation/promptBuilder.js';
|
||||
|
||||
let $editorModal = null;
|
||||
let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||
@@ -12,6 +12,7 @@ let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||
// Default prompts
|
||||
const DEFAULT_PROMPTS = {
|
||||
html: DEFAULT_HTML_PROMPT,
|
||||
spotify: DEFAULT_SPOTIFY_PROMPT,
|
||||
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
|
||||
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
|
||||
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons, but your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
||||
@@ -97,6 +98,7 @@ function openPromptsEditor() {
|
||||
// Create temporary copy for cancel functionality
|
||||
tempPrompts = {
|
||||
html: extensionSettings.customHtmlPrompt || '',
|
||||
spotify: extensionSettings.customSpotifyPrompt || '',
|
||||
plotRandom: extensionSettings.customPlotRandomPrompt || '',
|
||||
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
|
||||
avatar: extensionSettings.avatarLLMCustomInstruction || '',
|
||||
@@ -107,6 +109,7 @@ function openPromptsEditor() {
|
||||
|
||||
// Load current values or defaults
|
||||
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
|
||||
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
|
||||
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
|
||||
@@ -141,6 +144,7 @@ function closePromptsEditor() {
|
||||
*/
|
||||
function savePrompts() {
|
||||
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
|
||||
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
|
||||
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
|
||||
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
|
||||
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
|
||||
@@ -164,6 +168,9 @@ function restorePromptToDefault(promptType) {
|
||||
case 'html':
|
||||
extensionSettings.customHtmlPrompt = '';
|
||||
break;
|
||||
case 'spotify':
|
||||
extensionSettings.customSpotifyPrompt = '';
|
||||
break;
|
||||
case 'plotRandom':
|
||||
extensionSettings.customPlotRandomPrompt = '';
|
||||
break;
|
||||
@@ -192,6 +199,7 @@ function restorePromptToDefault(promptType) {
|
||||
*/
|
||||
function restoreAllToDefaults() {
|
||||
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
|
||||
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
|
||||
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
|
||||
@@ -201,6 +209,7 @@ function restoreAllToDefaults() {
|
||||
|
||||
// Clear all custom prompts
|
||||
extensionSettings.customHtmlPrompt = '';
|
||||
extensionSettings.customSpotifyPrompt = '';
|
||||
extensionSettings.customPlotRandomPrompt = '';
|
||||
extensionSettings.customPlotNaturalPrompt = '';
|
||||
extensionSettings.avatarLLMCustomInstruction = '';
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Snowflakes Effect Module
|
||||
* Creates and manages animated snowflakes overlay
|
||||
*/
|
||||
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
|
||||
let snowflakesContainer = null;
|
||||
|
||||
/**
|
||||
* Create snowflakes container and snowflakes
|
||||
*/
|
||||
function createSnowflakes() {
|
||||
if (snowflakesContainer) return; // Already created
|
||||
|
||||
// Create container
|
||||
snowflakesContainer = document.createElement('div');
|
||||
snowflakesContainer.className = 'rpg-snowflakes-container';
|
||||
|
||||
// Create 50 snowflakes with random positions
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.className = 'rpg-snowflake';
|
||||
snowflake.textContent = '❄';
|
||||
|
||||
// Random horizontal position
|
||||
snowflake.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
// Random animation delay for staggered effect
|
||||
snowflake.style.animationDelay = `${Math.random() * 10}s`;
|
||||
|
||||
// Random animation duration (between 10-20s)
|
||||
snowflake.style.animationDuration = `${10 + Math.random() * 10}s`;
|
||||
|
||||
snowflakesContainer.appendChild(snowflake);
|
||||
}
|
||||
|
||||
document.body.appendChild(snowflakesContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove snowflakes container
|
||||
*/
|
||||
function removeSnowflakes() {
|
||||
if (snowflakesContainer) {
|
||||
snowflakesContainer.remove();
|
||||
snowflakesContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle snowflakes effect
|
||||
*/
|
||||
export function toggleSnowflakes(enabled) {
|
||||
if (enabled) {
|
||||
createSnowflakes();
|
||||
} else {
|
||||
removeSnowflakes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize snowflakes based on saved state
|
||||
*/
|
||||
export function initSnowflakes() {
|
||||
const enabled = extensionSettings.enableSnowflakes || false;
|
||||
|
||||
if (enabled) {
|
||||
createSnowflakes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up snowflakes
|
||||
*/
|
||||
export function cleanupSnowflakes() {
|
||||
removeSnowflakes();
|
||||
}
|
||||
@@ -76,6 +76,27 @@ export function toggleAnimations() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates visibility of feature toggles in main panel based on settings
|
||||
*/
|
||||
export function updateFeatureTogglesVisibility() {
|
||||
const $featuresRow = $('#rpg-features-row');
|
||||
const $htmlToggle = $('#rpg-html-toggle-wrapper');
|
||||
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
|
||||
const $snowflakesToggle = $('#rpg-snowflakes-toggle-wrapper');
|
||||
|
||||
// Show/hide individual toggles
|
||||
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
|
||||
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
|
||||
$snowflakesToggle.toggle(extensionSettings.showSnowflakesToggle);
|
||||
|
||||
// Hide entire row if all toggles are hidden
|
||||
const anyVisible = extensionSettings.showHtmlToggle ||
|
||||
extensionSettings.showSpotifyToggle ||
|
||||
extensionSettings.showSnowflakesToggle;
|
||||
$featuresRow.toggle(anyVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings popup theme in real-time.
|
||||
* Backwards compatible wrapper for SettingsModal class.
|
||||
|
||||
@@ -1251,7 +1251,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
|
||||
/* Weather Widget Icon */
|
||||
.rpg-weather-icon {
|
||||
font-size: clamp(14px, 2.5vw, 20px);
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
@@ -1267,12 +1267,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
opacity: 0.85;
|
||||
line-height: 1.2;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
@@ -5120,19 +5117,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-weather-forecast {
|
||||
font-size: clamp(9px, 2.2vw, 11px) !important;
|
||||
font-size: clamp(0.5625rem, 2.2vw, 0.6875rem) !important;
|
||||
}
|
||||
|
||||
.rpg-temp-value {
|
||||
font-size: clamp(10px, 2.5vw, 13px) !important;
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.8125rem) !important;
|
||||
}
|
||||
|
||||
.rpg-time-value {
|
||||
font-size: clamp(10px, 2.5vw, 13px) !important;
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.8125rem) !important;
|
||||
}
|
||||
|
||||
.rpg-location-text {
|
||||
font-size: clamp(11px, 2.8vw, 14px) !important;
|
||||
font-size: clamp(0.6875rem, 2.8vw, 0.875rem) !important;
|
||||
}
|
||||
|
||||
.rpg-map-marker {
|
||||
@@ -8303,3 +8300,374 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SPOTIFY MUSIC WIDGET STYLES
|
||||
============================================ */
|
||||
|
||||
/* Music Widget Container - Positioned above chat input */
|
||||
.rpg-music-widget {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(-100% - 8px));
|
||||
width: calc(100% - 40px);
|
||||
max-width: 650px;
|
||||
z-index: 1000;
|
||||
animation: rpg-music-slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes rpg-music-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Widget Content - The actual player UI */
|
||||
.rpg-music-widget-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: var(--rpg-accent);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border: 2px solid var(--rpg-border);
|
||||
}
|
||||
|
||||
.rpg-music-widget-content:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 4px 8px var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* Spotify Icon */
|
||||
.rpg-music-widget-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #1DB954;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Song Info Container */
|
||||
.rpg-music-widget-info {
|
||||
flex: 1;
|
||||
min-width: 0; /* Allows text truncation */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Song Title */
|
||||
.rpg-music-widget-title {
|
||||
color: var(--rpg-text);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Artist Name */
|
||||
.rpg-music-widget-artist {
|
||||
color: var(--rpg-text);
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Play Button */
|
||||
.rpg-music-widget-play {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--rpg-highlight);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: var(--rpg-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.rpg-music-widget-play:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.rpg-music-widget-play:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.rpg-music-widget-play i {
|
||||
margin-left: 2px; /* Visual centering for play icon */
|
||||
}
|
||||
|
||||
/* Close/Dismiss Button */
|
||||
.rpg-music-widget-close {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: var(--rpg-text);
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.rpg-music-widget-close:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Mobile Responsive Styles */
|
||||
@media (max-width: 600px) {
|
||||
.rpg-music-widget-content {
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rpg-music-widget-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rpg-music-widget-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rpg-music-widget-artist {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.rpg-music-widget-play {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rpg-music-widget-close {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme Support - Apply theme colors */
|
||||
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-content,
|
||||
body[data-theme="sci-fi"] .rpg-music-widget-content {
|
||||
background: linear-gradient(135deg, #8b00ff 0%, #6a00cc 50%, #5000aa 100%);
|
||||
}
|
||||
|
||||
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-play,
|
||||
body[data-theme="sci-fi"] .rpg-music-widget-play {
|
||||
color: #8b00ff;
|
||||
}
|
||||
|
||||
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-content,
|
||||
body[data-theme="fantasy"] .rpg-music-widget-content {
|
||||
background: linear-gradient(135deg, #d4af37 0%, #b8962e 50%, #9c7d25 100%);
|
||||
}
|
||||
|
||||
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-play,
|
||||
body[data-theme="fantasy"] .rpg-music-widget-play {
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-content,
|
||||
body[data-theme="cyberpunk"] .rpg-music-widget-content {
|
||||
background: linear-gradient(135deg, #ff2a6d 0%, #d91e58 50%, #b31848 100%);
|
||||
}
|
||||
|
||||
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-play,
|
||||
body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
||||
color: #ff2a6d;
|
||||
}
|
||||
/* ============================================
|
||||
SNOWFLAKES EFFECT
|
||||
============================================ */
|
||||
|
||||
/* Snowflakes container - covers entire viewport */
|
||||
.rpg-snowflakes-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Individual snowflake */
|
||||
.rpg-snowflake {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
|
||||
animation: rpg-snowfall linear infinite;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Snowfall animation */
|
||||
@keyframes rpg-snowfall {
|
||||
0% {
|
||||
transform: translateY(0vh) rotate(0deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(360deg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create variations for different snowflakes */
|
||||
.rpg-snowflake:nth-child(2n) {
|
||||
animation-duration: 12s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.rpg-snowflake:nth-child(3n) {
|
||||
animation-duration: 15s;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.rpg-snowflake:nth-child(4n) {
|
||||
animation-duration: 18s;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.rpg-snowflake:nth-child(5n) {
|
||||
animation-duration: 10s;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
/* Slower mobile animation for performance */
|
||||
@media (max-width: 768px) {
|
||||
.rpg-snowflake {
|
||||
animation-duration: 20s;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HOLIDAY PROMOTION BANNER
|
||||
============================================ */
|
||||
|
||||
/* Mobile-friendly holiday promo */
|
||||
.rpg-holiday-promo {
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.rpg-holiday-promo a {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Dismiss button hover effect */
|
||||
#rpg-dismiss-promo:hover {
|
||||
opacity: 1 !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 600px) {
|
||||
.rpg-holiday-promo {
|
||||
font-size: 10px !important;
|
||||
padding: 10px 8px !important;
|
||||
}
|
||||
|
||||
#rpg-dismiss-promo {
|
||||
font-size: 12px !important;
|
||||
top: 2px !important;
|
||||
right: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
THREE-COLUMN FEATURE TOGGLES LAYOUT
|
||||
============================================ */
|
||||
|
||||
/* Features row container */
|
||||
.rpg-features-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Each feature column */
|
||||
.rpg-feature-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rpg-feature-col .rpg-toggle-label {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Always hide text, show only checkbox + icon */
|
||||
.rpg-feature-col .rpg-toggle-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rpg-feature-col .rpg-toggle-label i {
|
||||
font-size: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tablet and below: Hide text, show only checkbox + icon */
|
||||
@media (max-width: 768px) {
|
||||
.rpg-feature-col .rpg-toggle-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rpg-feature-col .rpg-toggle-label i {
|
||||
font-size: 1.cally with icons only */
|
||||
@media (max-width: 400px) {
|
||||
.rpg-features-row {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rpg-feature-col .rpg-toggle-label i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Ensure send_form has relative positioning for music widget placement */
|
||||
#send_form {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
+71
-4
@@ -10,7 +10,7 @@
|
||||
<div class="rpg-panel-header">
|
||||
<h3>
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span data-i18n-key="template.mainPanel.title">RPG Companion</span>
|
||||
<span id="rpg-panel-title" data-i18n-key="template.mainPanel.title">RPG Companion</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -60,17 +60,46 @@
|
||||
<div id="rpg-quests" class="rpg-section rpg-quests-section">
|
||||
<!-- Content will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Divider after Quests -->
|
||||
<div id="rpg-divider-quests" class="rpg-divider"></div>
|
||||
|
||||
<!-- Music Player Section -->
|
||||
<div id="rpg-music-player" class="rpg-section rpg-music-section">
|
||||
<!-- Content will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Toggles Row -->
|
||||
<div class="rpg-features-row" id="rpg-features-row">
|
||||
<!-- HTML Prompt Toggle -->
|
||||
<div class="rpg-toggle-container">
|
||||
<label class="rpg-toggle-label">
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-html-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Immersive HTML">
|
||||
<input type="checkbox" id="rpg-toggle-html-prompt">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
<span data-i18n-key="template.mainPanel.enableImmersiveHtml">Enable Immersive HTML</span>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.immersiveHtml">Immersive HTML</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Music Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-spotify-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Spotify Music">
|
||||
<input type="checkbox" id="rpg-toggle-spotify-music">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.spotifyMusic">Spotify Music</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Snowflakes Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-snowflakes-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Snowflakes Effect">
|
||||
<input type="checkbox" id="rpg-toggle-snowflakes">
|
||||
<i class="fa-solid fa-snowflake"></i>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.snowflakesEffect">Snowflakes Effect</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Update Button -->
|
||||
<button id="rpg-manual-update" class="rpg-btn-primary rpg-manual-update-btn">
|
||||
<i class="fa-solid fa-sync"></i> <span data-i18n-key="template.mainPanel.refreshRpgInfo">Refresh RPG
|
||||
@@ -88,6 +117,15 @@
|
||||
data-i18n-key="template.mainPanel.settingsButton">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Holiday Promotion -->
|
||||
<div class="rpg-holiday-promo" id="rpg-holiday-promo" style="text-align: center; padding: 12px 10px; margin-top: 8px; font-size: 11px; opacity: 0.85; position: relative; line-height: 1.5;">
|
||||
<button id="rpg-dismiss-promo" style="position: absolute; top: 4px; right: 4px; background: none; border: none; color: currentColor; opacity: 0.6; cursor: pointer; padding: 2px 6px; font-size: 14px; line-height: 1;" title="Dismiss permanently">✓</button>
|
||||
<div style="margin-bottom: 4px;">Happy Holidays & Happy New Year!</div>
|
||||
<a href="https://www.electronhub.ai/" target="_blank" style="color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; display: inline-block;">
|
||||
🎁 15% OFF for Electron Hub subscriptions with <strong>2026WITHMARINARA</strong> 🎁
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,6 +278,21 @@
|
||||
Smooth transitions for stats, content updates, and dice rolls
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-html-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showImmersiveHtmlToggle">Show Immersive HTML</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-spotify-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showSpotifyMusicToggle">Show Spotify Music</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-snowflakes-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showSnowflakesToggle">Show Snowflakes Effect</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-plot-buttons" />
|
||||
<span data-i18n-key="template.settingsModal.display.showPlotProgressionButtons">Show Plot
|
||||
@@ -666,6 +719,20 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Music Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-spotify" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
<i class="fa-brands fa-spotify"></i> Spotify Music Prompt
|
||||
</label>
|
||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
||||
Injected when "Enable Spotify Music" is enabled. Asks AI to suggest appropriate music for the scene.
|
||||
</small>
|
||||
<textarea id="rpg-prompt-spotify" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="spotify" style="margin-top: 8px;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Random Plot Progression Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
|
||||
Reference in New Issue
Block a user