您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a Location and Character System to JanitorAI with nested grouping functionality, enhancing organization and management.
当前为
// ==UserScript== // @name JanitorAI Context Maker with Nested Groups // @namespace http://tampermonkey.net/ // @version 3.0 // @license MIT // @description Adds a Location and Character System to JanitorAI with nested grouping functionality, enhancing organization and management. // @match https://janitorai.com/chats/* // @icon https://www.google.com/s2/favicons?sz=64&domain=https://janitorai.com/ // @grant GM.setValue // @grant GM.getValue // ==/UserScript== (async function() { 'use strict'; // Define Themes const themes = { dark: { '--bg-color': 'rgba(34, 34, 34, var(--ui-transparency))', '--text-color': '#ffffff', '--border-color': 'rgba(68, 68, 68, var(--ui-transparency))', '--button-bg-color': 'rgba(0, 123, 255, var(--ui-transparency))', '--active-char-color': 'rgba(173, 216, 230, var(--ui-transparency))', '--success-bg-color': 'rgba(40, 167, 69, var(--ui-transparency))', '--info-bg-color': 'rgba(23, 162, 184, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 193, 7, var(--ui-transparency))', '--muted-bg-color': 'rgba(108, 117, 125, var(--ui-transparency))', '--danger-bg-color': 'rgba(220, 53, 69, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#8cb3ff', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#ff9d00' }, light: { '--bg-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--text-color': '#000000', '--border-color': 'rgba(204, 204, 204, var(--ui-transparency))', '--button-bg-color': 'rgba(0, 123, 255, var(--ui-transparency))', '--active-char-color': 'rgba(173, 216, 230, var(--ui-transparency))', '--success-bg-color': 'rgba(40, 167, 69, var(--ui-transparency))', '--info-bg-color': 'rgba(23, 162, 184, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 193, 7, var(--ui-transparency))', '--muted-bg-color': 'rgba(108, 117, 125, var(--ui-transparency))', '--danger-bg-color': 'rgba(220, 53, 69, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.1)', '--link-color': '#007bff', '--code-bg-color': 'rgba(0, 0, 0, 0.05)', '--code-text-color': '#d63384' }, sepia_light: { '--bg-color': 'rgba(244, 232, 208, var(--ui-transparency))', '--text-color': '#2e241c', '--border-color': 'rgba(193, 154, 107, var(--ui-transparency))', '--button-bg-color': 'rgba(160, 82, 45, var(--ui-transparency))', '--active-char-color': 'rgba(210, 180, 140, var(--ui-transparency))', '--success-bg-color': 'rgba(107, 68, 35, var(--ui-transparency))', '--info-bg-color': 'rgba(194, 148, 110, var(--ui-transparency))', '--warning-bg-color': 'rgba(215, 172, 116, var(--ui-transparency))', '--muted-bg-color': 'rgba(160, 130, 94, var(--ui-transparency))', '--danger-bg-color': 'rgba(168, 96, 50, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.3)', '--link-color': '#6c757d', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#a0522d' }, sepia_dark: { '--bg-color': 'rgba(60, 45, 31, var(--ui-transparency))', '--text-color': '#d8c6b2', '--border-color': 'rgba(102, 75, 50, var(--ui-transparency))', '--button-bg-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--active-char-color': 'rgba(210, 180, 140, var(--ui-transparency))', '--success-bg-color': 'rgba(107, 68, 35, var(--ui-transparency))', '--info-bg-color': 'rgba(139, 101, 68, var(--ui-transparency))', '--warning-bg-color': 'rgba(205, 133, 63, var(--ui-transparency))', '--muted-bg-color': 'rgba(122, 91, 62, var(--ui-transparency))', '--danger-bg-color': 'rgba(165, 42, 42, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.6)', '--link-color': '#d2b48c', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#deb887' }, solarized_light: { '--bg-color': 'rgba(253, 246, 227, var(--ui-transparency))', '--text-color': '#47565c', '--border-color': 'rgba(238, 232, 213, var(--ui-transparency))', '--button-bg-color': 'rgba(38, 139, 210, var(--ui-transparency))', '--active-char-color': 'rgba(133, 153, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(133, 153, 0, var(--ui-transparency))', '--info-bg-color': 'rgba(38, 139, 210, var(--ui-transparency))', '--warning-bg-color': 'rgba(181, 137, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(147, 161, 161, var(--ui-transparency))', '--danger-bg-color': 'rgba(220, 50, 47, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#2aa198', '--code-bg-color': 'rgba(0, 43, 54, 0.7)', '--code-text-color': '#cb4b16' }, solarized_dark: { '--bg-color': 'rgba(0, 43, 54, var(--ui-transparency))', '--text-color': '#eee8d5', '--border-color': 'rgba(7, 54, 66, var(--ui-transparency))', '--button-bg-color': 'rgba(38, 139, 210, var(--ui-transparency))', '--active-char-color': 'rgba(133, 153, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(133, 153, 0, var(--ui-transparency))', '--info-bg-color': 'rgba(38, 139, 210, var(--ui-transparency))', '--warning-bg-color': 'rgba(181, 137, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(88, 110, 117, var(--ui-transparency))', '--danger-bg-color': 'rgba(220, 50, 47, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.4)', '--link-color': '#2aa198', '--code-bg-color': 'rgba(253, 246, 227, 0.1)', '--code-text-color': '#cb4b16' }, forest_light: { '--bg-color': 'rgba(233, 245, 233, var(--ui-transparency))', '--text-color': '#2f4f2f', '--border-color': 'rgba(209, 230, 209, var(--ui-transparency))', '--button-bg-color': 'rgba(60, 179, 113, var(--ui-transparency))', '--active-char-color': 'rgba(34, 139, 34, var(--ui-transparency))', '--success-bg-color': 'rgba(144, 238, 144, var(--ui-transparency))', '--info-bg-color': 'rgba(60, 179, 113, var(--ui-transparency))', '--warning-bg-color': 'rgba(240, 230, 140, var(--ui-transparency))', '--muted-bg-color': 'rgba(152, 251, 152, var(--ui-transparency))', '--danger-bg-color': 'rgba(205, 92, 92, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#3cb371', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#8b4513' }, forest_dark: { '--bg-color': 'rgba(34, 49, 34, var(--ui-transparency))', '--text-color': '#e0f7e9', '--border-color': 'rgba(46, 61, 46, var(--ui-transparency))', '--button-bg-color': 'rgba(60, 179, 113, var(--ui-transparency))', '--active-char-color': 'rgba(144, 238, 144, var(--ui-transparency))', '--success-bg-color': 'rgba(34, 139, 34, var(--ui-transparency))', '--info-bg-color': 'rgba(144, 238, 144, var(--ui-transparency))', '--warning-bg-color': 'rgba(189, 183, 107, var(--ui-transparency))', '--muted-bg-color': 'rgba(85, 107, 47, var(--ui-transparency))', '--danger-bg-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.4)', '--link-color': '#8fbc8f', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#ffd700' }, ocean_light: { '--bg-color': 'rgba(28, 107, 160, var(--ui-transparency))', '--text-color': '#ffffff', '--border-color': 'rgba(54, 144, 192, var(--ui-transparency))', '--button-bg-color': 'rgba(0, 123, 255, var(--ui-transparency))', '--active-char-color': 'rgba(173, 216, 230, var(--ui-transparency))', '--success-bg-color': 'rgba(60, 179, 113, var(--ui-transparency))', '--info-bg-color': 'rgba(23, 162, 184, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 193, 7, var(--ui-transparency))', '--muted-bg-color': 'rgba(108, 117, 125, var(--ui-transparency))', '--danger-bg-color': 'rgba(220, 53, 69, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.3)', '--link-color': '#00ffff', '--code-bg-color': 'rgba(0, 0, 0, 0.2)', '--code-text-color': '#ff7f50' }, ocean_dark: { '--bg-color': 'rgba(0, 30, 60, var(--ui-transparency))', '--text-color': '#ffffff', '--border-color': 'rgba(0, 53, 102, var(--ui-transparency))', '--button-bg-color': 'rgba(0, 123, 255, var(--ui-transparency))', '--active-char-color': 'rgba(135, 206, 235, var(--ui-transparency))', '--success-bg-color': 'rgba(46, 139, 87, var(--ui-transparency))', '--info-bg-color': 'rgba(0, 96, 100, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 140, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(47, 79, 79, var(--ui-transparency))', '--danger-bg-color': 'rgba(139, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#00ffff', '--code-bg-color': 'rgba(0, 0, 0, 0.4)', '--code-text-color': '#ff7f50' }, terminal: { '--bg-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--text-color': '#00ff00', '--border-color': 'rgba(0, 200, 0, var(--ui-transparency))', '--button-bg-color': 'rgba(0, 125, 0, var(--ui-transparency))', '--active-char-color': 'rgba(0, 75, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(0, 128, 0, var(--ui-transparency))', '--info-bg-color': 'rgba(0, 125, 0, var(--ui-transparency))', '--warning-bg-color': 'rgba(100, 125, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(0, 128, 0, var(--ui-transparency))', '--danger-bg-color': 'rgba(100, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0, 50, 0, 0.5)', '--link-color': '#00ffff', '--code-bg-color': 'rgba(0, 0, 0, 0.8)', '--code-text-color': '#7fff00' }, retro: { '--bg-color': 'rgba(196, 182, 187, var(--ui-transparency))', '--text-color': '#191919', '--border-color': 'rgba(128, 128, 128, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 105, 180, var(--ui-transparency))', '--active-char-color': 'rgba(255, 255, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(50, 205, 50, var(--ui-transparency))', '--info-bg-color': 'rgba(135, 206, 235, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 165, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(128, 128, 128, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.3)', '--link-color': '#1e90ff', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#ff1493' }, neon: { '--bg-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--text-color': '#ffffff', '--border-color': 'rgba(0, 255, 255, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 0, 255, var(--ui-transparency))', '--active-char-color': 'rgba(0, 255, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(0, 255, 0, var(--ui-transparency))', '--info-bg-color': 'rgba(0, 255, 255, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 255, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(255, 0, 255, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0, 255, 255, 0.5)', '--link-color': '#ff00ff', '--code-bg-color': 'rgba(0, 0, 0, 0.8)', '--code-text-color': '#00ffff' }, vintage: { '--bg-color': 'rgba(240, 230, 140, var(--ui-transparency))', '--text-color': '#360505', '--border-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--button-bg-color': 'rgba(128, 0, 0, var(--ui-transparency))', '--active-char-color': 'rgba(255, 215, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(50, 205, 50, var(--ui-transparency))', '--info-bg-color': 'rgba(70, 130, 180, var(--ui-transparency))', '--warning-bg-color': 'rgba(218, 165, 32, var(--ui-transparency))', '--muted-bg-color': 'rgba(128, 128, 0, var(--ui-transparency))', '--danger-bg-color': 'rgba(178, 34, 34, var(--ui-transparency))', '--shadow-color': 'rgba(139, 69, 19, 0.5)', '--link-color': '#8b0000', '--code-bg-color': 'rgba(245, 222, 179, 0.5)', '--code-text-color': '#8b0000' }, pastel: { '--bg-color': 'rgba(255, 228, 225, var(--ui-transparency))', '--text-color': '#4d4d4d', '--border-color': 'rgba(255, 192, 203, var(--ui-transparency))', '--button-bg-color': 'rgba(135, 206, 235, var(--ui-transparency))', '--active-char-color': 'rgba(175, 238, 238, var(--ui-transparency))', '--success-bg-color': 'rgba(152, 251, 152, var(--ui-transparency))', '--info-bg-color': 'rgba(135, 206, 235, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 228, 181, var(--ui-transparency))', '--muted-bg-color': 'rgba(238, 130, 238, var(--ui-transparency))', '--danger-bg-color': 'rgba(240, 128, 128, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.1)', '--link-color': '#ff69b4', '--code-bg-color': 'rgba(255, 250, 205, 0.8)', '--code-text-color': '#dc143c' } }; // Initialize CSS variables function setTheme(themeName) { const theme = themes[themeName]; Object.keys(theme).forEach(key => { document.documentElement.style.setProperty(key, theme[key]); }); } // Retrieve saved settings or set defaults const defaultTransparency = await GM.getValue('transparency', '0.9'); const defaultTheme = await GM.getValue('theme', 'dark'); // Initialize transparency and theme document.documentElement.style.setProperty('--ui-transparency', defaultTransparency); setTheme(defaultTheme); // Add placeholder styles const style = document.createElement('style'); style.innerHTML = ` input::placeholder, textarea::placeholder { color: var(--text-color); } input:-ms-input-placeholder, textarea:-ms-input-placeholder { color: var(--text-color); } input::-ms-input-placeholder, textarea::-ms-input-placeholder { color: var(--text-color); } input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { color: var(--text-color); } `; document.head.appendChild(style); // --- Context Menu and Tool Buttons --- const menuButtonsContainer = document.createElement('div'); menuButtonsContainer.style.cssText = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); display: flex; justify-content: center; z-index: 10000; `; document.body.appendChild(menuButtonsContainer); const contextMenuButton = document.createElement('button'); contextMenuButton.textContent = '⚙️'; contextMenuButton.title = 'Menu'; contextMenuButton.style.cssText = ` width: 40px; height: 40px; margin-right: 5px; padding: 0; border: none; background-color: var(--button-bg-color); color: var(--text-color); border-radius: 50%; font-size: 24px; cursor: pointer; z-index: 10000; `; menuButtonsContainer.appendChild(contextMenuButton); const toolButtonsData = [ { emoji: '🧮', title: 'Calculator', iframeSrc: 'https://www.desmos.com/fourfunction', id: 'calculator' }, { emoji: '🎲', title: 'Dice Roller', iframeSrc: '', id: 'dice' }, { emoji: '🎮', title: 'Game', iframeSrc: 'https://freepacman.org/', id: 'game' }, ]; toolButtonsData.forEach(tool => { const button = document.createElement('button'); button.textContent = tool.emoji; button.title = tool.title; button.style.cssText = ` width: 40px; height: 40px; margin-right: 5px; padding: 0; border: none; background-color: var(--button-bg-color); color: var(--text-color); border-radius: 50%; font-size: 24px; cursor: pointer; `; button.addEventListener('click', () => { openToolWindow(tool); }); menuButtonsContainer.appendChild(button); }); const openWindows = new Set(); function openToolWindow(tool) { if (openWindows.has(tool.id)) { return; } // Create the window container const windowContainer = document.createElement('div'); windowContainer.style.cssText = ` position: fixed; top: 100px; left: 100px; width: 500px; height: 400px; background-color: var(--bg-color); border: 1px solid var(--border-color); box-shadow: 0 0 10px var(--shadow-color); border-radius: 5px; z-index: 10002; display: flex; flex-direction: column; `; // Add a header with the title and close button const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 5px; background-color: var(--button-bg-color); color: var(--text-color); cursor: move; `; const title = document.createElement('span'); title.textContent = tool.title; header.appendChild(title); const closeButton = document.createElement('button'); closeButton.textContent = '✖️'; closeButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; `; closeButton.addEventListener('click', () => { document.body.removeChild(windowContainer); openWindows.delete(tool.id); }); header.appendChild(closeButton); windowContainer.appendChild(header); // Add iframe const iframe = document.createElement('iframe'); iframe.style.cssText = ` flex-grow: 1; width: 100%; border: none; background-color: var(--bg-color); color: var(--text-color); `; let src = tool.iframeSrc; // Adjust dice URL based on theme if (tool.id === 'dice') { const dicehex = getComputedHexValue('--button-bg-color').slice(1); const chromahex = getComputedHexValue('--bg-color').slice(1); const labelhex = getComputedHexValue('--text-color').slice(1); src = `https://dice.bee.ac/?dicehex=${dicehex}&chromahex=${chromahex}&labelhex=${labelhex}`; } // Adjust calculator URL based on theme if (tool.id === 'calculator') { const bgColor = getComputedHexValue('--bg-color').slice(1); const textColor = getComputedHexValue('--text-color').slice(1); const buttonBgColor = getComputedHexValue('--button-bg-color').slice(1); src = `https://www.desmos.com/fourfunction?backgroundColor=${bgColor}&textColor=${textColor}&buttonBackgroundColor=${buttonBgColor}`; } iframe.src = src; windowContainer.appendChild(iframe); // Make window draggable makeElementDraggable(windowContainer, header); document.body.appendChild(windowContainer); openWindows.add(tool.id); document.body.appendChild(windowContainer); openWindows.add(tool.id); } function getComputedHexValue(variableName) { const computedStyle = getComputedStyle(document.documentElement); const colorValue = computedStyle.getPropertyValue(variableName).trim(); // Create a temporary div to get the computed color const tempDiv = document.createElement('div'); tempDiv.style.color = colorValue; document.body.appendChild(tempDiv); const computedColor = getComputedStyle(tempDiv).color; document.body.removeChild(tempDiv); // Now computedColor is in 'rgb(r, g, b)' or 'rgba(r, g, b, a)' format const rgba = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (rgba) { const r = parseInt(rgba[1]).toString(16).padStart(2, '0'); const g = parseInt(rgba[2]).toString(16).padStart(2, '0'); const b = parseInt(rgba[3]).toString(16).padStart(2, '0'); return `#${r}${g}${b}`; } else { // Fallback to default return '#FFFFFF'; } } function makeElementDraggable(elmnt, handle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (handle) { handle.onmousedown = dragMouseDown; } else { elmnt.onmousedown = dragMouseDown; } function dragMouseDown(e) { e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // --- Context Panel --- const contextPanel = document.createElement('div'); contextPanel.id = 'context-panel'; contextPanel.style.cssText = ` position: fixed; /* Keep position fixed to center the panel */ top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400px; padding: 20px; background-color: var(--bg-color); border: 1px solid var(--border-color); box-shadow: 0 0 10px var(--shadow-color); border-radius: 5px; display: none; z-index: 10001; /* Remove position: relative; */ `; document.body.appendChild(contextPanel); // Add close button to contextPanel const contextCloseButton = document.createElement('button'); contextCloseButton.textContent = '✖️'; contextCloseButton.style.cssText = ` position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; contextCloseButton.addEventListener('click', () => { contextPanel.style.display = 'none'; }); contextPanel.appendChild(contextCloseButton); const transparencyLabel = document.createElement('label'); transparencyLabel.textContent = 'UI Transparency'; transparencyLabel.style.cssText = ` color: var(--text-color); display: block; margin-bottom: 5px; `; contextPanel.appendChild(transparencyLabel); const transparencySlider = document.createElement('input'); transparencySlider.type = 'range'; transparencySlider.min = '0.1'; transparencySlider.max = '1'; transparencySlider.step = '0.1'; transparencySlider.value = defaultTransparency; transparencySlider.style.cssText = ` width: 100%; margin-bottom: 10px; `; transparencySlider.addEventListener('input', () => { document.documentElement.style.setProperty('--ui-transparency', transparencySlider.value); // Re-apply current theme to update transparency setTheme(themeDropdown.value); GM.setValue('transparency', transparencySlider.value); }); contextPanel.appendChild(transparencySlider); const dropperLabel = document.createElement('label'); dropperLabel.textContent = 'UI Theme'; dropperLabel.style.cssText = ` color: var(--text-color); display: block; margin-bottom: 5px; `; contextPanel.appendChild(dropperLabel); const themeDropdown = document.createElement('select'); themeDropdown.style.cssText = ` width: 100%; padding: 5px; margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); `; Object.keys(themes).forEach(theme => { const option = document.createElement('option'); option.value = theme; option.textContent = theme.charAt(0).toUpperCase() + theme.slice(1); themeDropdown.appendChild(option); }); themeDropdown.value = defaultTheme; themeDropdown.addEventListener('change', () => { setTheme(themeDropdown.value); GM.setValue('theme', themeDropdown.value); }); contextPanel.appendChild(themeDropdown); // Add divider const divider = document.createElement('hr'); divider.style.cssText = ` border: none; border-top: 1px solid var(--border-color); margin: 10px 0; `; contextPanel.appendChild(divider); const contextLabel = document.createElement('label'); contextLabel.textContent = 'Primary Context'; contextLabel.style.cssText = ` color: var(--text-color); display: block; margin-bottom: 5px; `; contextPanel.appendChild(contextLabel); const primaryContextInput = document.createElement('textarea'); primaryContextInput.placeholder = 'Primary context goes here...'; primaryContextInput.style.cssText = ` width: 100%; height: 100px; margin-bottom: 10px; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; box-sizing: border-box; background-color: var(--bg-color); color: var(--text-color); `; contextPanel.appendChild(primaryContextInput); // Define the sleep function function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const updateContextButton = document.createElement('button'); updateContextButton.textContent = 'Update Context'; updateContextButton.style.cssText = ` width: 100%; padding: 10px; border: none; background-color: var(--success-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; updateContextButton.addEventListener('click', async () => { const primaryContext = primaryContextInput.value; var fullContext = `Context;\n"` + primaryContext + `"\n`; fullContext += `\n` + window.getContext(); console.log("setting context; " + fullContext); window.manuClick('//*[@id="menu-button-:rb:"]'); await sleep(250); window.manuClick('//*[@id="menu-list-:rb:-menuitem-:ru:"]'); await sleep(250); window.manuWrite("/html/body/div[9]/div[3]/div/section/div/textarea", fullContext); await sleep(250); window.manuClick("/html/body/div[9]/div[3]/div/section/footer/button[2]"); }); window.manuWrite = function(xpath, text) { var result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); var node = result.singleNodeValue; if (node) { node.focus(); // Since the node is an HTMLTextAreaElement, get the value setter from its prototype const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, 'value' ).set; nativeTextAreaValueSetter.call(node, text); // Dispatch the 'input' event to simulate user input var event = new Event('input', { bubbles: true }); node.dispatchEvent(event); } else { console.error("Node not found for XPath:", xpath); } } window.manuClick = function(xpath) { var result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); var node = result.singleNodeValue; if (node) { node.click(); } } contextPanel.appendChild(updateContextButton); contextMenuButton.addEventListener('click', () => { contextPanel.style.display = contextPanel.style.display === 'none' ? 'block' : 'none'; }); // Helper function to find the active location recursively function findActiveLocation(items) { for (const item of items) { if (item.active) { return item; } if (item.children && item.children.length > 0) { const activeChild = findActiveLocation(item.children); if (activeChild) { return activeChild; } } } return null; } // Helper function to collect all active characters recursively function collectActiveCharacters(items, result = []) { for (const item of items) { if (item.active) { result.push(item); } if (item.children && item.children.length > 0) { collectActiveCharacters(item.children, result); } } return result; } // Modify the getContext function to utilize the helper functions window.getContext = function() { const activeLocation = findActiveLocation(locationGroups.flatMap(g => g.items)); if (!activeLocation) return ''; let context = ``; if (plotInput && plotInput.value.trim() !== '') { context += `Plot:\n"${plotInput.value}"\n`; } else { context += `Plot:\n"No specific plot."\n`; } context += `\nSetting;\n"${activeLocation.name}" (${timeInput.value}, ${weatherInput.value}):\n` context += `\u0009"${activeLocation.description}"\n`; context += `\n{{user}}'s characters;\n`; const activeUserCharacters = collectActiveCharacters(characterGroups.flatMap(g => g.items)).filter(char => char.userControlled); activeUserCharacters.forEach(char => { context += `"${char.name}" (${char.sex}, ${char.species}, ${char.age}, ${char.bodyType}, ${char.personality}):\n`; context += `\u0009"${char.bio}"\n`; }); context += `\n{{char}}'s characters;\n`; const activeCharCharacters = collectActiveCharacters(characterGroups.flatMap(g => g.items)).filter(char => !char.userControlled); activeCharCharacters.forEach(char => { context += `"${char.name}" (${char.sex}, ${char.species}, ${char.age}, ${char.bodyType}, ${char.personality}):\n`; context += `\u0009"${char.bio}"\n`; }); return context; }; // --- Character Sidebar with Groups and Nested Characters --- const characterSidebar = document.createElement('div'); characterSidebar.id = 'character-sheet-sidebar'; characterSidebar.style.cssText = ` position: fixed; top: 0; right: -350px; width: 350px; height: 100%; display: flex; flex-direction: column; background-color: var(--bg-color); border-left: 2px solid var(--border-color); box-shadow: -2px 0 5px var(--shadow-color); box-sizing: border-box; transition: right 0.3s; z-index: 9999; `; document.body.appendChild(characterSidebar); const characterToggleButton = document.createElement('button'); characterToggleButton.textContent = '☰'; characterToggleButton.style.cssText = ` position: fixed; top: 10px; right: 10px; padding: 5px 10px; border: none; background-color: var(--button-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; transition: right 0.3s; z-index: 10000; `; characterToggleButton.addEventListener('click', () => { const isOpen = characterSidebar.style.right === '0px'; characterSidebar.style.right = isOpen ? '-350px' : '0'; characterToggleButton.style.right = isOpen ? '10px' : '360px'; }); document.body.appendChild(characterToggleButton); const characterHeader = document.createElement('div'); characterHeader.style.cssText = ` padding: 10px; background-color: var(--border-color); text-align: center; font-weight: bold; color: var(--text-color); `; characterHeader.textContent = 'Characters'; characterSidebar.appendChild(characterHeader); const characterButtonBox = document.createElement('div'); characterButtonBox.style.cssText = ` display: flex; justify-content: space-between; padding: 10px; background-color: var(--bg-color); flex-shrink: 0; `; characterSidebar.appendChild(characterButtonBox); // New Character Button const newCharacterButton = document.createElement('button'); newCharacterButton.textContent = 'New'; newCharacterButton.title = 'Create New Character'; newCharacterButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--success-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; newCharacterButton.addEventListener('click', () => { createNewCharacter(); }); characterButtonBox.appendChild(newCharacterButton); // Load Character Button const loadCharacterButton = document.createElement('button'); loadCharacterButton.textContent = 'Load'; loadCharacterButton.title = 'Load a Character'; loadCharacterButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--info-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; loadCharacterButton.addEventListener('click', () => { loadCharacter(); }); characterButtonBox.appendChild(loadCharacterButton); // Group Character Button const groupCharacterButton = document.createElement('button'); groupCharacterButton.textContent = 'Group'; groupCharacterButton.title = 'Create New Group'; groupCharacterButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--info-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; groupCharacterButton.addEventListener('click', () => { createNewCharacterGroup(); }); characterButtonBox.appendChild(groupCharacterButton); // Import Character Button const importCharacterButton = document.createElement('button'); importCharacterButton.textContent = 'Import'; importCharacterButton.title = 'Import Characters'; importCharacterButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--warning-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; importCharacterButton.addEventListener('click', () => { importCharacters(); }); characterButtonBox.appendChild(importCharacterButton); // Export Character Button const exportCharacterButton = document.createElement('button'); exportCharacterButton.textContent = 'Export'; exportCharacterButton.title = 'Export Characters'; exportCharacterButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--muted-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; exportCharacterButton.addEventListener('click', () => { exportCharacters(); }); characterButtonBox.appendChild(exportCharacterButton); // Add fifth button: "Load" for Character function loadCharacter() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { try { const character = JSON.parse(e.target.result); if (character && character.id) { // Add to Ungrouped const ungrouped = characterGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items.push(character); } else { characterGroups[0].items.push(character); } renderCharacterGroups(); } else { alert('Invalid character format.'); } } catch (error) { alert('Failed to load character: ' + error.message); } }; reader.readAsText(file); }); fileInput.click(); } const characterGroupList = document.createElement('div'); characterGroupList.id = 'character-group-list'; characterGroupList.style.cssText = ` flex-grow: 1; overflow-y: auto; padding: 10px; `; characterSidebar.appendChild(characterGroupList); // Initialize Character Groups let characterGroups = [ { name: 'Ungrouped', items: [], collapsed: false } ]; // Function to create a new Character function createNewCharacter() { const newCharacter = { id: generateId(), active: true, emoji: '😀', name: 'New Character', sex: '', species: '', age: '', bodyType: '', personality: '', bio: '', userControlled: false, children: [], collapsed: false }; // Add to Ungrouped const ungrouped = characterGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items.push(newCharacter); } else { characterGroups[0].items.push(newCharacter); } renderCharacterGroups(); } // Function to create a new Character Group function createNewCharacterGroup() { const groupName = prompt('Enter group name:', `Group ${characterGroups.length}`); if (groupName && groupName.trim() !== '') { characterGroups.push({ name: groupName.trim(), items: [], collapsed: false }); renderCharacterGroups(); } } // Function to render Character Groups and Items function renderCharacterGroups() { characterGroupList.innerHTML = ''; characterGroups.forEach((group, groupIndex) => { // Sort items alphabetically by name group.items.sort((a, b) => a.name.localeCompare(b.name)); const groupContainer = document.createElement('div'); groupContainer.className = 'character-group'; groupContainer.style.cssText = ` margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--active-char-color); `; const groupHeader = document.createElement('div'); groupHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; background-color: var(--button-bg-color); color: var(--text-color); cursor: pointer; user-select: none; position: relative; `; groupHeader.textContent = group.name; groupHeader.addEventListener('click', () => { group.collapsed = !group.collapsed; renderCharacterGroups(); }); const groupActions = document.createElement('div'); groupActions.style.cssText = ` display: flex; align-items: center; `; // Activate/Deactivate All Button const toggleAllButton = document.createElement('button'); toggleAllButton.title = 'Activate/Deactivate All'; toggleAllButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; font-size: 16px; `; updateToggleAllButton(toggleAllButton, group); toggleAllButton.addEventListener('click', (e) => { e.stopPropagation(); toggleAllGroupCharacters(group); updateToggleAllButton(toggleAllButton, group); renderCharacterGroups(); }); groupActions.appendChild(toggleAllButton); // Rename Group Button const renameGroupButton = document.createElement('button'); renameGroupButton.textContent = '✏️'; renameGroupButton.title = 'Rename Group'; renameGroupButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; renameGroupButton.addEventListener('click', (e) => { e.stopPropagation(); renameGroup(groupIndex); }); groupActions.appendChild(renameGroupButton); // Delete Group Button const deleteGroupButton = document.createElement('button'); deleteGroupButton.textContent = '🗑️'; deleteGroupButton.title = 'Delete Group'; deleteGroupButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; `; deleteGroupButton.addEventListener('click', (e) => { e.stopPropagation(); deleteGroup(groupIndex); }); groupActions.appendChild(deleteGroupButton); groupHeader.appendChild(groupActions); groupContainer.appendChild(groupHeader); if (!group.collapsed) { const itemsContainer = document.createElement('div'); itemsContainer.style.cssText = ` padding: 5px 10px; display: block; `; if (group.items.length === 0) { const emptyInfo = document.createElement('div'); emptyInfo.className = 'character-slot'; emptyInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; border: 1px solid var(--border-color); border-radius: 0px; height: 20px; margin-bottom: 5px; background-color: var(--bg-color); cursor: default; `; const infoText = document.createElement('span'); infoText.textContent = `Group is empty`; infoText.style.cssText = ` color: var(--text-color); `; emptyInfo.appendChild(infoText); itemsContainer.appendChild(emptyInfo); } else { group.items.forEach((character, charIndex) => { const characterSlot = createCharacterSlot(character, groupIndex); itemsContainer.appendChild(characterSlot); }); } groupContainer.appendChild(itemsContainer); } else { const collapsedInfo = document.createElement('div'); collapsedInfo.className = 'character-slot'; collapsedInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: 5px; height: 20px; margin-top: 5px; background-color: var(--bg-color); cursor: default; margin-left: 10px; margin-right: 10px; `; const infoText = document.createElement('span'); infoText.textContent = `${group.items.length} items hidden`; infoText.style.cssText = ` color: var(--text-color); `; collapsedInfo.appendChild(infoText); groupContainer.appendChild(collapsedInfo); } // Add dragover and drop events for grouping groupContainer.addEventListener('dragover', (e) => { e.preventDefault(); groupContainer.style.border = `2px dashed var(--info-bg-color)`; }); groupContainer.addEventListener('dragleave', (e) => { groupContainer.style.border = `1px solid var(--border-color)`; }); groupContainer.addEventListener('drop', (e) => { e.preventDefault(); groupContainer.style.border = `1px solid var(--border-color)`; const data = e.dataTransfer.getData('text/plain'); const { type, id } = JSON.parse(data); if (type === 'character') { moveCharacterToGroup(id, groupIndex); } }); characterGroupList.appendChild(groupContainer); }); // Ensure at least one group exists if (characterGroups.length === 0) { characterGroups.push({ name: 'Ungrouped', items: [], collapsed: false }); renderCharacterGroups(); } } // Function to update the Toggle All Button appearance function updateToggleAllButton(button, group) { const activeCount = countActiveCharacters(group.items); const totalCount = countTotalCharacters(group.items); if (activeCount === totalCount && totalCount > 0) { // All active button.textContent = '🟢'; } else if (activeCount === 0) { // All inactive button.textContent = '🔴'; } else { // Mixed button.textContent = '🟡'; } } function countActiveCharacters(items) { let count = 0; items.forEach(item => { if (item.active) count++; if (item.children && item.children.length > 0) { count += countActiveCharacters(item.children); } }); return count; } function countTotalCharacters(items) { let count = items.length; items.forEach(item => { if (item.children && item.children.length > 0) { count += countTotalCharacters(item.children); } }); return count; } // Function to toggle all characters in a group function toggleAllGroupCharacters(group) { const allActive = countActiveCharacters(group.items) === countTotalCharacters(group.items); toggleCharacters(group.items, !allActive); } function toggleCharacters(items, state) { items.forEach(item => { item.active = state; if (item.children && item.children.length > 0) { toggleCharacters(item.children, state); } }); } // Function to create a Character Slot DOM element (recursive) function createCharacterSlot(character, groupIndex, parent = null, level = 0) { const characterSlot = document.createElement('div'); characterSlot.className = 'character-slot'; characterSlot.draggable = true; characterSlot.dataset.id = character.id; characterSlot.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 5px; margin-bottom: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: ${character.userControlled ? 'var(--active-char-color)' : 'var(--bg-color)'}; cursor: grab; margin-left: ${level * 20}px; `; // Drag events characterSlot.addEventListener('dragstart', (e) => { e.stopPropagation(); e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'character', id: character.id })); e.currentTarget.style.opacity = '0.5'; }); characterSlot.addEventListener('dragend', (e) => { e.currentTarget.style.opacity = '1'; }); // Click event for collapsing/expanding if (character.children && character.children.length > 0) { characterSlot.style.cursor = 'pointer'; characterSlot.addEventListener('click', (e) => { e.stopPropagation(); character.collapsed = !character.collapsed; renderCharacterGroups(); }); } // Left Div (Active Toggle, Emoji, Name) const leftDiv = document.createElement('div'); leftDiv.style.cssText = ` display: flex; align-items: center; flex-grow: 1; overflow: hidden; `; // Remove Collapse/Expand Button // (Not needed as per new requirement) const activeButton = document.createElement('button'); activeButton.textContent = character.active ? '🟢' : '🔴'; activeButton.title = 'Toggle Active'; activeButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; activeButton.addEventListener('click', (e) => { e.stopPropagation(); character.active = !character.active; updateAllToggleButtons(); renderCharacterGroups(); }); leftDiv.appendChild(activeButton); const emojiSpan = document.createElement('span'); emojiSpan.textContent = character.emoji; emojiSpan.style.marginRight = '5px'; leftDiv.appendChild(emojiSpan); const nameSpan = document.createElement('span'); nameSpan.textContent = character.name; nameSpan.style.cssText = ` flex-grow: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-color); `; leftDiv.appendChild(nameSpan); characterSlot.appendChild(leftDiv); // Right Div (Edit, Delete, Export) const rightDiv = document.createElement('div'); rightDiv.style.display = 'flex'; rightDiv.style.alignItems = 'center'; const editButton = document.createElement('button'); editButton.textContent = '✏️'; editButton.title = 'Edit Character'; editButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; editButton.addEventListener('click', (e) => { e.stopPropagation(); editCharacter(character); }); rightDiv.appendChild(editButton); const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.title = 'Delete Character'; deleteButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; deleteButton.addEventListener('click', (e) => { e.stopPropagation(); deleteCharacter(character.id); }); rightDiv.appendChild(deleteButton); const exportButton = document.createElement('button'); exportButton.textContent = '⬇️'; exportButton.title = 'Export Character'; exportButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); `; exportButton.addEventListener('click', (e) => { e.stopPropagation(); exportCharacter(character); }); rightDiv.appendChild(exportButton); characterSlot.appendChild(rightDiv); // Dragover and Drop events characterSlot.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); characterSlot.style.border = `2px dashed var(--info-bg-color)`; }); characterSlot.addEventListener('dragleave', (e) => { characterSlot.style.border = `1px solid var(--border-color)`; }); characterSlot.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); characterSlot.style.border = `1px solid var(--border-color)`; const data = e.dataTransfer.getData('text/plain'); const { type, id } = JSON.parse(data); if (type === 'character' && id !== character.id) { moveCharacterToParent(id, character); } }); // Prevent click on buttons from triggering slot collapse/expand activeButton.addEventListener('click', (e) => e.stopPropagation()); editButton.addEventListener('click', (e) => e.stopPropagation()); deleteButton.addEventListener('click', (e) => e.stopPropagation()); exportButton.addEventListener('click', (e) => e.stopPropagation()); // Recursive rendering of children const container = document.createElement('div'); container.appendChild(characterSlot); if (character.children && character.children.length > 0) { if (!character.collapsed) { character.children.forEach(child => { const childSlot = createCharacterSlot(child, groupIndex, character, level + 1); container.appendChild(childSlot); }); } else { const collapsedInfo = document.createElement('div'); collapsedInfo.className = 'character-slot'; collapsedInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; margin-bottom: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); height: 20px; cursor: default; margin-left: ${(level + 1) * 20}px; `; const infoText = document.createElement('span'); infoText.textContent = `${character.children.length} items hidden`; infoText.style.cssText = ` color: var(--text-color); `; collapsedInfo.appendChild(infoText); container.appendChild(collapsedInfo); } } return container; } function updateAllToggleButtons() { characterGroups.forEach((group, groupIndex) => { const toggleAllButton = toggleAllButtonForGroup(groupIndex); if (toggleAllButton) { updateToggleAllButton(toggleAllButton, group); } }); } // Function to get the toggle all button for a group function toggleAllButtonForGroup(groupIndex) { const groupContainer = characterGroupList.children[groupIndex]; if (groupContainer) { const toggleAllButton = groupContainer.querySelector('button[title="Activate/Deactivate All"]'); return toggleAllButton; } return null; } // Function to rename a group function renameGroup(groupIndex) { const newName = prompt('Enter new group name:', characterGroups[groupIndex].name); if (newName && newName.trim() !== '') { characterGroups[groupIndex].name = newName.trim(); renderCharacterGroups(); } } // Function to delete a group function deleteGroup(groupIndex) { if (characterGroups.length === 1) { alert('At least one group must exist.'); return; } if (confirm(`Are you sure you want to delete the group "${characterGroups[groupIndex].name}"? All characters in this group will be moved to "Ungrouped".`)) { const group = characterGroups.splice(groupIndex, 1)[0]; const ungrouped = characterGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items = ungrouped.items.concat(group.items); } else { characterGroups.unshift({ name: 'Ungrouped', items: group.items, collapsed: false }); } renderCharacterGroups(); } } // Function to edit a character function editCharacter(character) { const editForm = document.createElement('form'); editForm.innerHTML = ` <div style="width: 100%; min-width: 500px; margin: 0 auto;"> <label style="color: var(--text-color); font-size: 12px;">Emoji: <input type="text" name="emoji" value="${character.emoji}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Name: <input type="text" name="name" value="${character.name}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Sex: <input type="text" name="sex" value="${character.sex}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Species: <input type="text" name="species" value="${character.species}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Age: <input type="text" name="age" value="${character.age}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Body Type: <input type="text" name="bodyType" value="${character.bodyType}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Personality: <input type="text" name="personality" value="${character.personality}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Bio: <textarea name="bio" autocomplete="off" rows="4" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;">${character.bio}</textarea></label><br> <label style="color: var(--text-color); font-size: 12px;">User Controlled: <input type="checkbox" name="userControlled" ${character.userControlled ? 'checked' : ''}></label><br> <button type="submit" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 5px; border-radius: 5px; width: 100%; text-align: center; margin-top: 10px;">Save</button> </div> `; const modal = createModal('Edit Character', editForm); editForm.addEventListener('submit', (e) => { e.preventDefault(); character.emoji = editForm.emoji.value; character.name = editForm.name.value; character.sex = editForm.sex.value; character.species = editForm.species.value; character.age = editForm.age.value; character.bodyType = editForm.bodyType.value; character.personality = editForm.personality.value; character.bio = editForm.bio.value; character.userControlled = editForm.userControlled.checked; renderCharacterGroups(); closeModal(modal); }); } // Function to delete a character function deleteCharacter(characterId) { if (confirm('Are you sure you want to delete this character?')) { characterGroups.forEach(group => { group.items = deleteCharacterRecursive(group.items, characterId); }); renderCharacterGroups(); } } function deleteCharacterRecursive(items, characterId) { return items.filter(item => { if (item.id === characterId) { return false; } if (item.children && item.children.length > 0) { item.children = deleteCharacterRecursive(item.children, characterId); } return true; }); } // Function to export all characters with groups function exportCharacters() { const data = characterGroups; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `all_characters_with_groups.json`; link.click(); } // Function to import characters with groups function importCharacters() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { try { const importedGroups = JSON.parse(e.target.result); if (Array.isArray(importedGroups)) { characterGroups = importedGroups; renderCharacterGroups(); } else { alert('Invalid format for characters import.'); } } catch (error) { alert('Failed to import characters: ' + error.message); } }; reader.readAsText(file); }); fileInput.click(); } // Function to export a single character function exportCharacter(character) { const json = JSON.stringify(character, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${character.name}.json`; link.click(); } // Function to move character to a group (uncoupling from parent) function moveCharacterToGroup(characterId, targetGroupIndex) { let character = null; // Remove character from current group or parent characterGroups.forEach(group => { group.items = removeCharacterRecursive(group.items, characterId, (item) => { character = item; }); }); // Add to target group if (character) { characterGroups[targetGroupIndex].items.push(character); renderCharacterGroups(); } } function removeCharacterRecursive(items, characterId, callback) { return items.filter(item => { if (item.id === characterId) { callback(item); return false; } if (item.children && item.children.length > 0) { item.children = removeCharacterRecursive(item.children, characterId, callback); } return true; }); } // Function to move character under another character (nesting) function moveCharacterToParent(characterId, parentCharacter) { let character = null; // Remove character from current group or parent characterGroups.forEach(group => { group.items = removeCharacterRecursive(group.items, characterId, (item) => { character = item; }); }); if (character) { parentCharacter.children.push(character); renderCharacterGroups(); } } // Utility function to generate unique IDs function generateId() { return '_' + Math.random().toString(36).substr(2, 9); } // --- Location Sidebar with Groups and Nested Locations --- const locationSidebar = document.createElement('div'); locationSidebar.id = 'location-sheet-sidebar'; locationSidebar.style.cssText = ` position: fixed; top: 0; left: -350px; width: 350px; height: 100%; display: flex; flex-direction: column; background-color: var(--bg-color); border-right: 2px solid var(--border-color); box-shadow: 2px 0 5px var(--shadow-color); box-sizing: border-box; transition: left 0.3s; z-index: 9999; `; document.body.appendChild(locationSidebar); const locationToggleButton = document.createElement('button'); locationToggleButton.textContent = '☰'; locationToggleButton.style.cssText = ` position: fixed; top: 10px; left: 10px; padding: 5px 10px; border: none; background-color: var(--button-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; transition: left 0.3s; z-index: 10000; `; locationToggleButton.addEventListener('click', () => { const isOpen = locationSidebar.style.left === '0px'; locationSidebar.style.left = isOpen ? '-350px' : '0'; locationToggleButton.style.left = isOpen ? '10px' : '360px'; }); document.body.appendChild(locationToggleButton); const locationHeader = document.createElement('div'); locationHeader.style.cssText = ` padding: 10px; background-color: var(--border-color); text-align: center; font-weight: bold; color: var(--text-color); `; locationHeader.textContent = 'Locations'; locationSidebar.appendChild(locationHeader); const locationButtonBox = document.createElement('div'); locationButtonBox.style.cssText = ` display: flex; justify-content: space-between; padding: 10px; background-color: var(--bg-color); flex-shrink: 0; `; locationSidebar.appendChild(locationButtonBox); // New Location Button const newLocationButton = document.createElement('button'); newLocationButton.textContent = 'New'; newLocationButton.title = 'Create New Location'; newLocationButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--success-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; newLocationButton.addEventListener('click', () => { createNewLocation(); }); locationButtonBox.appendChild(newLocationButton); // Load Location Button const loadLocationButton = document.createElement('button'); loadLocationButton.textContent = 'Load'; loadLocationButton.title = 'Load a Location'; loadLocationButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--info-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; loadLocationButton.addEventListener('click', () => { loadLocation(); }); locationButtonBox.appendChild(loadLocationButton); // Group Location Button const groupLocationButton = document.createElement('button'); groupLocationButton.textContent = 'Group'; groupLocationButton.title = 'Create New Group'; groupLocationButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--info-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; groupLocationButton.addEventListener('click', () => { createNewLocationGroup(); }); locationButtonBox.appendChild(groupLocationButton); // Import Location Button const importLocationButton = document.createElement('button'); importLocationButton.textContent = 'Import'; importLocationButton.title = 'Import Locations'; importLocationButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--warning-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; importLocationButton.addEventListener('click', () => { importLocations(); }); locationButtonBox.appendChild(importLocationButton); // Export Location Button const exportLocationButton = document.createElement('button'); exportLocationButton.textContent = 'Export'; exportLocationButton.title = 'Export Locations'; exportLocationButton.style.cssText = ` padding: 5px 10px; border: none; background-color: var(--muted-bg-color); color: var(--text-color); border-radius: 5px; cursor: pointer; `; exportLocationButton.addEventListener('click', () => { exportLocations(); }); locationButtonBox.appendChild(exportLocationButton); // Add fifth button: "Load" for Location function loadLocation() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { try { const location = JSON.parse(e.target.result); if (location && location.id) { // Add to Ungrouped const ungrouped = locationGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items.push(location); } else { locationGroups[0].items.push(location); } renderLocationGroups(); ensureActiveLocation(); } else { alert('Invalid location format.'); } } catch (error) { alert('Failed to load location: ' + error.message); } }; reader.readAsText(file); }); fileInput.click(); } const locationGroupList = document.createElement('div'); locationGroupList.id = 'location-group-list'; locationGroupList.style.cssText = ` flex-grow: 1; overflow-y: auto; padding: 10px; `; locationSidebar.appendChild(locationGroupList); // Initialize Location Groups with a single default location let locationGroups = [ { name: 'Ungrouped', items: [ { id: generateId(), active: true, emoji: '📍', name: 'Default Location', description: 'Description of the default location.', children: [], collapsed: false } ], collapsed: false } ]; // Function to create a new Location function createNewLocation() { const newLocation = { id: generateId(), active: false, emoji: '📍', name: 'New Location', description: '', children: [], collapsed: false }; // Add to Ungrouped const ungrouped = locationGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items.push(newLocation); } else { locationGroups[0].items.push(newLocation); } renderLocationGroups(); ensureActiveLocation(); } // Function to create a new Location Group function createNewLocationGroup() { const groupName = prompt('Enter group name:', `Group ${locationGroups.length}`); if (groupName && groupName.trim() !== '') { locationGroups.push({ name: groupName.trim(), items: [], collapsed: false }); renderLocationGroups(); } } // Function to render Location Groups and Items function renderLocationGroups() { locationGroupList.innerHTML = ''; locationGroups.forEach((group, groupIndex) => { // Sort items alphabetically by name group.items.sort((a, b) => a.name.localeCompare(b.name)); const groupContainer = document.createElement('div'); groupContainer.className = 'location-group'; groupContainer.style.cssText = ` margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--active-char-color); `; const groupHeader = document.createElement('div'); groupHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; background-color: var(--button-bg-color); color: var(--text-color); cursor: pointer; user-select: none; position: relative; `; groupHeader.textContent = group.name; groupHeader.addEventListener('click', () => { group.collapsed = !group.collapsed; renderLocationGroups(); }); const groupActions = document.createElement('div'); groupActions.style.cssText = ` display: flex; align-items: center; `; // Rename Group Button const renameGroupButton = document.createElement('button'); renameGroupButton.textContent = '✏️'; renameGroupButton.title = 'Rename Group'; renameGroupButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; renameGroupButton.addEventListener('click', (e) => { e.stopPropagation(); renameLocationGroup(groupIndex); }); groupActions.appendChild(renameGroupButton); // Delete Group Button const deleteGroupButton = document.createElement('button'); deleteGroupButton.textContent = '🗑️'; deleteGroupButton.title = 'Delete Group'; deleteGroupButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; `; deleteGroupButton.addEventListener('click', (e) => { e.stopPropagation(); deleteLocationGroup(groupIndex); }); groupActions.appendChild(deleteGroupButton); groupHeader.appendChild(groupActions); groupContainer.appendChild(groupHeader); if (!group.collapsed) { const itemsContainer = document.createElement('div'); itemsContainer.style.cssText = ` padding: 5px 10px; display: block; `; if (group.items.length === 0) { const emptyInfo = document.createElement('div'); emptyInfo.className = 'location-slot'; emptyInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; margin-bottom: 5px; border: 1px solid var(--border-color); border-radius: 5px; height: 20px; background-color: var(--bg-color); cursor: default; `; const infoText = document.createElement('span'); infoText.textContent = `Group is empty`; infoText.style.cssText = ` color: var(--text-color); `; emptyInfo.appendChild(infoText); itemsContainer.appendChild(emptyInfo); } else { group.items.forEach((location) => { const locationSlot = createLocationSlot(location, groupIndex); itemsContainer.appendChild(locationSlot); }); } groupContainer.appendChild(itemsContainer); } else { const collapsedInfo = document.createElement('div'); collapsedInfo.className = 'location-slot'; collapsedInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: 5px; height: 20px; margin-top: 5px; background-color: var(--bg-color); cursor: default; margin-left: 10px; margin-right: 10px; `; const infoText = document.createElement('span'); infoText.textContent = `${group.items.length} items hidden`; infoText.style.cssText = ` color: var(--text-color); `; collapsedInfo.appendChild(infoText); groupContainer.appendChild(collapsedInfo); } // Add dragover and drop events for grouping groupContainer.addEventListener('dragover', (e) => { e.preventDefault(); groupContainer.style.border = `2px dashed var(--info-bg-color)`; }); groupContainer.addEventListener('dragleave', (e) => { groupContainer.style.border = `1px solid var(--border-color)`; }); groupContainer.addEventListener('drop', (e) => { e.preventDefault(); groupContainer.style.border = `1px solid var(--border-color)`; const data = e.dataTransfer.getData('text/plain'); const { type, id } = JSON.parse(data); if (type === 'location') { moveLocationToGroup(id, groupIndex); } }); locationGroupList.appendChild(groupContainer); }); // Ensure at least one group exists if (locationGroups.length === 0) { locationGroups.push({ name: 'Ungrouped', items: [], collapsed: false }); renderLocationGroups(); } // Ensure at least one location is active ensureActiveLocation(); } // Function to create a Location Slot DOM element (recursive) function createLocationSlot(location, groupIndex, parent = null, level = 0) { const locationSlot = document.createElement('div'); locationSlot.className = 'location-slot'; locationSlot.draggable = true; locationSlot.dataset.id = location.id; locationSlot.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 5px; margin-bottom: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); cursor: grab; margin-left: ${level * 20}px; `; // Drag events locationSlot.addEventListener('dragstart', (e) => { e.stopPropagation(); e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'location', id: location.id })); e.currentTarget.style.opacity = '0.5'; }); locationSlot.addEventListener('dragend', (e) => { e.currentTarget.style.opacity = '1'; }); // Click event for collapsing/expanding if (location.children && location.children.length > 0) { locationSlot.style.cursor = 'pointer'; locationSlot.addEventListener('click', (e) => { e.stopPropagation(); location.collapsed = !location.collapsed; renderLocationGroups(); }); } // Left Div (Active Toggle, Emoji, Name) const leftDiv = document.createElement('div'); leftDiv.style.cssText = ` display: flex; align-items: center; flex-grow: 1; overflow: hidden; `; // Remove Collapse/Expand Button // (Not needed as per new requirement) const activeButton = document.createElement('button'); activeButton.textContent = location.active ? '🟢' : '🔴'; activeButton.title = 'Toggle Active'; activeButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; activeButton.addEventListener('click', (e) => { e.stopPropagation(); // Since only one active location is allowed, deactivate others locationGroups.forEach(group => { group.items.forEach(loc => { deactivateLocationRecursive(loc); }); }); location.active = !location.active; renderLocationGroups(); }); leftDiv.appendChild(activeButton); const emojiSpan = document.createElement('span'); emojiSpan.textContent = location.emoji; emojiSpan.style.marginRight = '5px'; leftDiv.appendChild(emojiSpan); const nameSpan = document.createElement('span'); nameSpan.textContent = location.name; nameSpan.style.cssText = ` flex-grow: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-color); `; leftDiv.appendChild(nameSpan); locationSlot.appendChild(leftDiv); // Right Div (Edit, Delete, Export) const rightDiv = document.createElement('div'); rightDiv.style.display = 'flex'; rightDiv.style.alignItems = 'center'; const editButton = document.createElement('button'); editButton.textContent = '✏️'; editButton.title = 'Edit Location'; editButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; editButton.addEventListener('click', (e) => { e.stopPropagation(); editLocation(location); }); rightDiv.appendChild(editButton); const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.title = 'Delete Location'; deleteButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 5px; `; deleteButton.addEventListener('click', (e) => { e.stopPropagation(); deleteLocation(location.id); }); rightDiv.appendChild(deleteButton); const exportButton = document.createElement('button'); exportButton.textContent = '⬇️'; exportButton.title = 'Export Location'; exportButton.style.cssText = ` padding: 3px; border: none; background: none; cursor: pointer; color: var(--text-color); `; exportButton.addEventListener('click', (e) => { e.stopPropagation(); exportLocation(location); }); rightDiv.appendChild(exportButton); locationSlot.appendChild(rightDiv); // Dragover and Drop events locationSlot.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); locationSlot.style.border = `2px dashed var(--info-bg-color)`; }); locationSlot.addEventListener('dragleave', (e) => { locationSlot.style.border = `1px solid var(--border-color)`; }); locationSlot.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); locationSlot.style.border = `1px solid var(--border-color)`; const data = e.dataTransfer.getData('text/plain'); const { type, id } = JSON.parse(data); if (type === 'location' && id !== location.id) { moveLocationToParent(id, location); } }); // Prevent click on buttons from triggering slot collapse/expand activeButton.addEventListener('click', (e) => e.stopPropagation()); editButton.addEventListener('click', (e) => e.stopPropagation()); deleteButton.addEventListener('click', (e) => e.stopPropagation()); exportButton.addEventListener('click', (e) => e.stopPropagation()); // Recursive rendering of children const container = document.createElement('div'); container.appendChild(locationSlot); if (location.children && location.children.length > 0) { if (!location.collapsed) { location.children.forEach(child => { const childSlot = createLocationSlot(child, groupIndex, location, level + 1); container.appendChild(childSlot); }); } else { const collapsedInfo = document.createElement('div'); collapsedInfo.className = 'location-slot'; collapsedInfo.style.cssText = ` display: flex; align-items: center; padding: 10px; margin-bottom: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); height: 20px; cursor: default; margin-left: ${(level + 1) * 20}px; `; const infoText = document.createElement('span'); infoText.textContent = `${location.children.length} items hidden`; infoText.style.cssText = ` color: var(--text-color); `; collapsedInfo.appendChild(infoText); container.appendChild(collapsedInfo); } } return container; } function deactivateLocationRecursive(location) { location.active = false; if (location.children && location.children.length > 0) { location.children.forEach(child => { deactivateLocationRecursive(child); }); } } // Function to rename a location group function renameLocationGroup(groupIndex) { const newName = prompt('Enter new group name:', locationGroups[groupIndex].name); if (newName && newName.trim() !== '') { locationGroups[groupIndex].name = newName.trim(); renderLocationGroups(); } } // Function to delete a location group function deleteLocationGroup(groupIndex) { if (locationGroups.length === 1) { alert('At least one group must exist.'); return; } if (confirm(`Are you sure you want to delete the group "${locationGroups[groupIndex].name}"? All locations in this group will be moved to "Ungrouped".`)) { const group = locationGroups.splice(groupIndex, 1)[0]; const ungrouped = locationGroups.find(g => g.name === 'Ungrouped'); if (ungrouped) { ungrouped.items = ungrouped.items.concat(group.items); } else { locationGroups.unshift({ name: 'Ungrouped', items: group.items, collapsed: false }); } renderLocationGroups(); } } // Function to edit a location function editLocation(location) { const editForm = document.createElement('form'); editForm.innerHTML = ` <div style="width: 100%; min-width: 500px; margin: 0 auto;"> <label style="color: var(--text-color); font-size: 12px;">Emoji: <input type="text" name="emoji" value="${location.emoji}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Name: <input type="text" name="name" value="${location.name}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br> <label style="color: var(--text-color); font-size: 12px;">Description: <textarea name="description" autocomplete="off" rows="4" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;">${location.description}</textarea></label><br> <button type="submit" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 5px; border-radius: 5px; width: 100%; text-align: center; margin-top: 10px;">Save</button> </div> `; const modal = createModal('Edit Location', editForm); editForm.addEventListener('submit', (e) => { e.preventDefault(); location.emoji = editForm.emoji.value; location.name = editForm.name.value; location.description = editForm.description.value; renderLocationGroups(); ensureActiveLocation(); closeModal(modal); }); } // Function to delete a location function deleteLocation(locationId) { if (confirm('Are you sure you want to delete this location?')) { locationGroups.forEach(group => { group.items = deleteLocationRecursive(group.items, locationId); }); renderLocationGroups(); ensureActiveLocation(); } } function deleteLocationRecursive(items, locationId) { return items.filter(item => { if (item.id === locationId) { return false; } if (item.children && item.children.length > 0) { item.children = deleteLocationRecursive(item.children, locationId); } return true; }); } // Function to export all locations with groups function exportLocations() { const data = locationGroups; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `all_locations_with_groups.json`; link.click(); } // Function to import locations with groups function importLocations() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { try { const importedGroups = JSON.parse(e.target.result); if (Array.isArray(importedGroups)) { locationGroups = importedGroups; renderLocationGroups(); ensureActiveLocation(); } else { alert('Invalid format for locations import.'); } } catch (error) { alert('Failed to import locations: ' + error.message); } }; reader.readAsText(file); }); fileInput.click(); } // Function to export a single location function exportLocation(location) { const json = JSON.stringify(location, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${location.name}.json`; link.click(); } // Function to move location to a group (uncoupling from parent) function moveLocationToGroup(locationId, targetGroupIndex) { let location = null; // Remove location from current group or parent locationGroups.forEach(group => { group.items = removeLocationRecursive(group.items, locationId, (item) => { location = item; }); }); // Add to target group if (location) { locationGroups[targetGroupIndex].items.push(location); renderLocationGroups(); ensureActiveLocation(); } } function removeLocationRecursive(items, locationId, callback) { return items.filter(item => { if (item.id === locationId) { callback(item); return false; } if (item.children && item.children.length > 0) { item.children = removeLocationRecursive(item.children, locationId, callback); } return true; }); } // Function to move location under another location (nesting) function moveLocationToParent(locationId, parentLocation) { let location = null; // Remove location from current group or parent locationGroups.forEach(group => { group.items = removeLocationRecursive(group.items, locationId, (item) => { location = item; }); }); if (location) { if(parentLocation.children == undefined){ parentLocation.children = []; } parentLocation.children.push(location); renderLocationGroups(); ensureActiveLocation(); } } // Function to create a modal function createModal(title, content) { const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 10002; `; const modalBox = document.createElement('div'); modalBox.style.cssText = ` background-color: var(--bg-color); padding: 20px; border: 1px solid var(--border-color); border-radius: 5px; width: fit-content; min-width: 500px; max-width: 90%; box-shadow: 0 0 10px var(--shadow-color); position: relative; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = title; modalTitle.style.cssText = ` margin-top: 0; color: var(--text-color); `; modalBox.appendChild(modalTitle); const closeButton = document.createElement('button'); closeButton.textContent = '✖️'; closeButton.style.cssText = ` position: absolute; top: 15px; right: 20px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; closeButton.addEventListener('click', () => { closeModal(modalOverlay); }); modalBox.appendChild(closeButton); modalBox.appendChild(content); modalOverlay.appendChild(modalBox); document.body.appendChild(modalOverlay); return modalOverlay; } // Function to close a modal function closeModal(modal) { if (modal && modal.parentNode) { modal.parentNode.removeChild(modal); } } // Function to ensure at least one location is active function ensureActiveLocation() { const activeLocations = []; locationGroups.forEach(group => { group.items.forEach(loc => { collectActiveLocations(loc, activeLocations); }); }); if (activeLocations.length === 0 && locationGroups.flatMap(g => g.items).length > 0) { locationGroups[0].items[0].active = true; } } function collectActiveLocations(location, activeLocations) { if (location.active) { activeLocations.push(location); } if (location.children && location.children.length > 0) { location.children.forEach(child => { collectActiveLocations(child, activeLocations); }); } } // Initial render of Character and Location Groups renderCharacterGroups(); renderLocationGroups(); // --- Plot Field --- const plotDiv = document.createElement('div'); plotDiv.style.cssText = ` display: flex; justify-content: space-between; padding: 10px; background-color: var(--bg-color); border-top: 1px solid var(--border-color); flex-shrink: 0; `; const plotInput = document.createElement('input'); plotInput.type = 'text'; plotInput.placeholder = 'Plot'; plotInput.style.cssText = ` width: 100%; padding: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); box-sizing: border-box; `; plotDiv.appendChild(plotInput); characterSidebar.appendChild(plotDiv); // --- Location Context Inputs --- const globalInputs = document.createElement('div'); globalInputs.style.cssText = ` display: flex; justify-content: space-between; padding: 10px; background-color: var(--bg-color); border-top: 1px solid var(--border-color); flex-shrink: 0; `; locationSidebar.appendChild(globalInputs); const timeInput = document.createElement('input'); timeInput.type = 'text'; timeInput.placeholder = 'Time'; timeInput.style.cssText = ` width: calc(50% - 5px); padding: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); `; globalInputs.appendChild(timeInput); const weatherInput = document.createElement('input'); weatherInput.type = 'text'; weatherInput.placeholder = 'Weather'; weatherInput.style.cssText = ` width: calc(50% - 5px); padding: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); `; globalInputs.appendChild(weatherInput); // Initial render of transparency and theme settings transparencySlider.value = defaultTransparency; themeDropdown.value = defaultTheme; })();