您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a Location and Character System to JanitorAI with nested grouping functionality
当前为
// ==UserScript== // @name JanitorAI Context Maker // @namespace http://tampermonkey.net/ // @version 4.1 // @license MIT // @description Adds a Location and Character System to JanitorAI with nested grouping functionality // @match https://janitorai.com/chats/* // @icon https://www.google.com/s2/favicons?sz=64&domain=https://janitorai.com/ // @grant GM.setValue // @grant GM.getValue // @grant GM.xmlHttpRequest // @connect cdnjs.cloudflare.com // ==/UserScript== (async function() { 'use strict'; // Define Themes const themes = { dark: { '--bg-color': 'rgba(34, 34, 34, var(--ui-transparency))', '--bg-tool': 'rgba(34, 34, 34, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--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))', '--bg-tool': 'rgba(255, 255, 255, 0.8)', '--text-color': '#000000', '--text-color-darker': '#333333', '--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))', '--bg-tool': 'rgba(244, 232, 208, 0.8', '--text-color': '#2e241c', '--text-color-darker': '#1a140f', '--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))', '--bg-tool': 'rgba(60, 45, 31, 0.8)', '--text-color': '#d8c6b2', '--text-color-darker': '#b8a493', '--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))', '--bg-tool': 'rgba(253, 246, 227, 0.8)', '--text-color': '#47565c', '--text-color-darker': '#2c3438', '--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))', '--bg-tool': 'rgba(0, 43, 54, 0.8)', '--text-color': '#eee8d5', '--text-color-darker': '#cdc5b0', '--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))', '--bg-tool': 'rgba(233, 245, 233, 0.8)', '--text-color': '#2f4f2f', '--text-color-darker': '#1c2f1c', '--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))', '--bg-tool': 'rgba(34, 49, 34, 0.8)', '--text-color': '#e0f7e9', '--text-color-darker': '#b0c7b9', '--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))', '--bg-tool': 'rgba(28, 107, 160, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--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))', '--bg-tool': 'rgba(0, 30, 60, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--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))', '--bg-tool': 'rgba(0, 0, 0, 0.8)', '--text-color': '#00ff00', '--text-color-darker': '#00cc00', '--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))', '--bg-tool': 'rgba(196, 182, 187, 0.8)', '--text-color': '#191919', '--text-color-darker': '#000000', '--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))', '--bg-tool': 'rgba(0, 0, 0, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--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))', '--bg-tool': 'rgba(240, 230, 140, 0.8)', '--text-color': '#360505', '--text-color-darker': '#200303', '--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))', '--bg-tool': 'rgba(255, 228, 225, 0.8)', '--text-color': '#4d4d4d', '--text-color-darker': '#333333', '--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; } } // Helper functions for tooltip let tooltipElement = null; function showTooltip(event, content, set) { hideTooltip(); // Remove existing tooltip if any // Create new tooltip element tooltipElement = document.createElement('div'); tooltipElement.style.cssText = ` position: absolute; z-index: 10000; background-color: var(--bg-tool); color: var(--text-color); border: 1px solid var(--border-color); padding: 5px; border-radius: 5px; font-size: 12px; max-width: 600px; white-space: pre-wrap; box-shadow: 0 0 10px var(--shadow-color); `; tooltipElement.innerHTML = content; // Append the tooltip to the body first to get its size document.body.appendChild(tooltipElement); // Get the parent node's bounding rectangle const parentRect = event.currentTarget.parentNode.parentNode.getBoundingClientRect(); if (!set) { // Tooltip to the right of the parent element tooltipElement.style.left = `${parentRect.right + 4}px`; tooltipElement.style.top = `${parentRect.top}px`; } else { // Tooltip to the left of the parent element const tooltipWidth = tooltipElement.offsetWidth; // Get the tooltip width after it's appended tooltipElement.style.left = `${parentRect.left - tooltipWidth + 15}px`; // Adjust to the left tooltipElement.style.top = `${parentRect.top}px`; } // Adjust position if the tooltip goes off screen const tooltipRect = tooltipElement.getBoundingClientRect(); if (tooltipRect.right > window.innerWidth) { tooltipElement.style.left = `${window.innerWidth - tooltipRect.width - 10}px`; } if (tooltipRect.left < 0) { tooltipElement.style.left = `10px`; // Move it a bit to the right if it goes off screen on the left } if (tooltipRect.bottom > window.innerHeight) { tooltipElement.style.top = `${window.innerHeight - tooltipRect.height - 10}px`; } } function hideTooltip() { if (tooltipElement && tooltipElement.parentNode) { tooltipElement.parentNode.removeChild(tooltipElement); tooltipElement = null; // Set to null after removing } } // --- 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.characterType == 1); 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.characterType == 0); 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: '', characterType: 0, 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: 3px; 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: 2px; `; 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`; //lime const activeCount = countActiveCharacters(group.items); const totalCount = countTotalCharacters(group.items); if (activeCount === totalCount && totalCount > 0) { // All active infoText.style.cssText = `color: rgba(36, 242, 0, 0.75);`; } else if (activeCount === 0) { // All inactive infoText.style.cssText = `color: var(--text-color-darker);`; } else { // Mixed infoText.style.cssText = `color: rgba(242, 198, 0, 0.75);`; } 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 generate tooltip content for a character function getCharacterTooltipContent(character, isRadial = false) { if (isRadial && character.emoji === '📁') { // If it's a group node in the radial tree, only show the name return `<strong>"${character.name}"</strong>`; } var content = `<strong>"${character.name}"</strong>`; if(character.characterType != 2){ content += ` <small>(${character.sex || 'Unknown'}, ${character.species || 'Unknown'}, ${character.age || 'Unknown'}, ${character.bodyType || 'Unknown'}, ${character.personality || 'Unknown'})</small>`; } content += `:<br><em>"${character.bio || 'No bio available.'}"</em>`; return content; } // 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.characterType == 1 ? '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); if(character.characterType == 2) { activeButton.style.display = 'none'; } 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); `; // Add tooltip event listeners nameSpan.addEventListener('mouseenter', (e) => { showTooltip(e, getCharacterTooltipContent(character), true); }); nameSpan.addEventListener('mouseleave', hideTooltip); nameSpan.addEventListener('dragover', hideTooltip); 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: 0px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 2px; `; 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: 0px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 2px; `; 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: 0px; 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`; //lime const activeCount = countActiveCharacters(character.children); const totalCount = countTotalCharacters(character.children); if (activeCount === totalCount && totalCount > 0) { // All active infoText.style.cssText = `color: rgba(36, 242, 0, 0.75);`; } else if (activeCount === 0) { // All inactive infoText.style.cssText = `color: var(--text-color-darker);`; } else { // Mixed infoText.style.cssText = `color: rgba(242, 198, 0, 0.75);`; } 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;">Character Type: <select name="characterType" 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;"> <option value="0" ${character.characterType === 0 ? 'selected' : ''}>Non-User</option> <option value="1" ${character.characterType === 1 ? 'selected' : ''}>User</option> <option value="2" ${character.characterType === 2 ? 'selected' : ''}>Collection</option> </select> </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> `; // Copy Tooltip button const copyButton = document.createElement('button'); copyButton.textContent = '📋'; copyButton.title = 'Copy Tooltip'; copyButton.style.cssText = ` position: absolute; top: 15px; right: 50px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; copyButton.addEventListener('click', () => { const tooltipContent = getCharacterTooltipContent(character); const textWithNewlines = tooltipContent.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ''); navigator.clipboard.writeText(textWithNewlines); }); editForm.appendChild(copyButton); 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.characterType = parseInt(editForm.characterType.value, 10); 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: characterGroups, settings: { c_radius, c_labelFontSize } // Save current settings }; 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 settings 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 importedData = JSON.parse(e.target.result); if (Array.isArray(importedData)) { // Old format characterGroups = importedData; renderCharacterGroups(); } else if (importedData.characterGroups) { characterGroups = importedData.characterGroups; // Load settings if (importedData.settings) { // Assign settings c_radius = importedData.settings.c_radius || c_radius; c_labelFontSize = importedData.settings.c_labelFontSize || c_labelFontSize; } 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: 2px; `; 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`; //lime if (findActiveLocation(group.items)) { // All active infoText.style.cssText = `color: rgba(36, 242, 0, 0.75);`; } else { // All inactive infoText.style.cssText = `color: var(--text-color-darker);`; } 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 generate tooltip content for a location function getLocationTooltipContent(location, isRadial = false) { if (isRadial && location.emoji === '📁') { // If it's a group node in the radial tree, only show the name return `<strong>"${location.name}"</strong>`; } let content = `<strong>"${location.name}"</strong>:<br><em>"${location.description || 'No description available.'}"</em>`; return content; } // 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); `; // Add tooltip event listeners nameSpan.addEventListener('mouseenter', (e) => { showTooltip(e, getLocationTooltipContent(location)); }); nameSpan.addEventListener('mouseleave', hideTooltip); nameSpan.addEventListener('dragover', hideTooltip); 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: 0px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 2px; `; 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: 0px; border: none; background: none; cursor: pointer; color: var(--text-color); margin-right: 2px; `; 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: 0px; 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`; //lime if (findActiveLocation(location.children)) { // All active infoText.style.cssText = `color: rgba(36, 242, 0, 0.75);`; } else { // All inactive infoText.style.cssText = `color: var(--text-color-darker);`; } 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> `; // Copy Tooltip button const copyButton = document.createElement('button'); copyButton.textContent = '📋'; copyButton.title = 'Copy Tooltip'; copyButton.style.cssText = ` position: absolute; top: 15px; right: 50px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; copyButton.addEventListener('click', () => { const tooltipContent = getLocationTooltipContent(location); const textWithNewlines = tooltipContent.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ''); navigator.clipboard.writeText(textWithNewlines); }); editForm.appendChild(copyButton); 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: locationGroups, settings: { l_radius, l_labelFontSize } // Save current settings }; 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 importedData = JSON.parse(e.target.result); if (Array.isArray(importedData)) { // Old format locationGroups = importedData; renderLocationGroups(); } else if (importedData.locationGroups) { locationGroups = importedData.locationGroups; // Load settings if (importedData.settings) { // Assign settings l_radius = importedData.settings.l_radius || l_radius; l_labelFontSize = importedData.settings.l_labelFontSize || l_labelFontSize; } renderLocationGroups(); } 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 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; // Load D3.js library await new Promise((resolve, reject) => { const d3Script = document.createElement('script'); d3Script.src = 'https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.4/d3.min.js'; d3Script.onload = resolve; d3Script.onerror = reject; document.head.appendChild(d3Script); }); // --- Modifications Start Here --- // Variables to keep track of open windows let radialTreeOpen = false; let searchPanelOpen = { character: false, location: false }; // Function to create a radial tree visualization function createRadialTree(data, type) { // Variables to store settings let c_radius = type === 'character' ? (window.c_radius || 300) : 300; let c_labelFontSize = type === 'character' ? (window.c_labelFontSize || 12) : 12; let c_nodeSize = type === 'character' ? (window.c_nodeSize || 24) : 24; let l_radius = type === 'location' ? (window.l_radius || 300) : 300; let l_labelFontSize = type === 'location' ? (window.l_labelFontSize || 12) : 12; let l_nodeSize = type === 'location' ? (window.l_nodeSize || 24) : 24; // Close any existing radial tree window if (document.getElementById('radial-tree-window')) { document.body.removeChild(document.getElementById('radial-tree-window')); } // Create the movable window container const windowContainer = document.createElement('div'); windowContainer.id = 'radial-tree-window'; windowContainer.style.cssText = ` position: fixed; top: 100px; left: 100px; width: 600px; height: 600px; 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 buttons const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; padding: 5px; background-color: var(--button-bg-color); color: var(--text-color); cursor: move; `; const title = document.createElement('span'); title.textContent = `${type === 'character' ? 'Character' : 'Location'} Radial Tree`; title.style.flexGrow = '1'; header.appendChild(title); const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.alignItems = 'center'; const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙️'; settingsButton.title = 'Settings'; settingsButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; buttonContainer.appendChild(settingsButton); 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); radialTreeOpen = false; }); buttonContainer.appendChild(closeButton); header.appendChild(buttonContainer); windowContainer.appendChild(header); // Create SVG container const svgContainer = document.createElement('div'); svgContainer.style.cssText = ` flex-grow: 1; width: 100%; background-color: var(--bg-color); overflow: hidden; position: relative; `; windowContainer.appendChild(svgContainer); // Append to body document.body.appendChild(windowContainer); radialTreeOpen = true; // Make window draggable makeElementDraggable(windowContainer, header); // Create settings panel const settingsPanel = document.createElement('div'); settingsPanel.style.cssText = ` position: absolute; top: 50px; right: 10px; width: 200px; background-color: var(--bg-color); border: 1px solid var(--border-color); box-shadow: 0 0 10px var(--shadow-color); border-radius: 5px; padding: 10px; z-index: 10003; display: none; `; settingsPanel.innerHTML = ` <label style="color: var(--text-color);">Node Distance: <input type="range" min="100" max="1000" value="${type === 'character' ? c_radius : l_radius}" id="nodeDistanceSlider" style="width: 100%;"></label> <label style="color: var(--text-color);">Label Size: <input type="range" min="8" max="24" value="${type === 'character' ? c_labelFontSize : l_labelFontSize}" id="labelSizeSlider" style="width: 100%;"></label> <label style="color: var(--text-color);">Node Size: <input type="range" min="8" max="48" value="${type === 'character' ? c_nodeSize : l_nodeSize}" id="nodeSizeSlider" style="width: 100%;"></label> <button id="applySettingsButton" style="margin-top: 10px; width: 100%;">Apply</button> `; windowContainer.appendChild(settingsPanel); settingsButton.addEventListener('click', () => { settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; }); const nodeDistanceSlider = settingsPanel.querySelector('#nodeDistanceSlider'); const labelSizeSlider = settingsPanel.querySelector('#labelSizeSlider'); const nodeSizeSlider = settingsPanel.querySelector('#nodeSizeSlider'); const applySettingsButton = settingsPanel.querySelector('#applySettingsButton'); applySettingsButton.addEventListener('click', () => { if (type === 'character') { c_radius = parseInt(nodeDistanceSlider.value, 10); c_labelFontSize = parseInt(labelSizeSlider.value, 10); c_nodeSize = parseInt(nodeSizeSlider.value, 10); window.c_radius = c_radius; // Store the settings window.c_labelFontSize = c_labelFontSize; window.c_nodeSize = c_nodeSize; } else { l_radius = parseInt(nodeDistanceSlider.value, 10); l_labelFontSize = parseInt(labelSizeSlider.value, 10); l_nodeSize = parseInt(nodeSizeSlider.value, 10); window.l_radius = l_radius; // Store the settings window.l_labelFontSize = l_labelFontSize; window.l_nodeSize = l_nodeSize; } settingsPanel.style.display = 'none'; // Regenerate the tree with new settings generateRadialTreeVisualization(svgContainer, data, type, type === 'character' ? c_radius : l_radius, type === 'character' ? c_labelFontSize : l_labelFontSize, type === 'character' ? c_nodeSize : l_nodeSize); }); // Initial rendering of the tree generateRadialTreeVisualization(svgContainer, data, type, type === 'character' ? c_radius : l_radius, type === 'character' ? c_labelFontSize : l_labelFontSize, type === 'character' ? c_nodeSize : l_nodeSize); } function generateRadialTreeVisualization(container, data, type, radius = 300, labelFontSize = 12, nodeSize = 24) { // Clear the container container.innerHTML = ''; // Set dimensions const width = container.clientWidth; const height = container.clientHeight; // Current rotation state (in degrees) let currentRotation = 0; // Store the current zoom and pan transformation let currentTransform = { x: width / 2, y: height / 2, k: 1 }; // Create a D3 zoom behavior const zoomBehavior = d3.zoom() .scaleExtent([0.5, 5]) .on('zoom', zoomed); // Create SVG element with zoom and pan functionality const svg = d3.select(container) .append('svg') .attr('width', width) .attr('height', height) .call(zoomBehavior) // Apply the zoom behavior .append('g') .attr('transform', `translate(${currentTransform.x},${currentTransform.y}) scale(${currentTransform.k}) rotate(${currentRotation})`); // Set the initial zoom transform to center the graph d3.select(container).select('svg').call(zoomBehavior.transform, d3.zoomIdentity.translate(width / 2, height / 2)); function zoomed(event) { // Update the current pan and zoom values currentTransform = event.transform; // Apply the combined transformation (zoom, pan, and rotation) svg.attr('transform', `translate(${currentTransform.x},${currentTransform.y}) scale(${currentTransform.k}) rotate(${currentRotation})`); } // Convert data to D3 hierarchy const root = d3.hierarchy({ name: type === 'character' ? 'Characters' : 'Locations', children: data }) .sum(d => d.children ? 0 : 1); // Create the radial tree layout const tree = d3.tree() .size([2 * Math.PI, radius]) .separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth); tree(root); // Create links const link = svg.append('g') .selectAll('.link') .data(root.links()) .join('path') .attr('class', 'link') .attr('d', d3.linkRadial() .angle(d => d.x) .radius(d => d.y)) .attr('stroke', 'var(--border-color)') .attr('fill', 'none'); // Create nodes const node = svg.append('g') .selectAll('.node') .data(root.descendants()) .join('g') .attr('class', 'node') .attr('transform', d => ` rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0) `); // Use emoji as node symbols node.append('text') .attr('dy', '0.31em') .attr('font-size', `${nodeSize}px`) // Use nodeSize for font size .attr('text-anchor', 'middle') .attr('transform', d => `rotate(0)`) // No flipping of text .text(d => d.data.emoji || '⭕') // Use emoji or a default symbol .style('font-size', `${nodeSize}px`) .style('fill', 'var(--text-color)') .on('mouseover', (event, d) => { const content = type === 'character' ? getCharacterTooltipContent(d.data, true) : getLocationTooltipContent(d.data, true); showTooltip_radial(event, content); }) .on('mousemove', (event) => { moveTooltip_radial(event); }) .on('mouseout', hideTooltip_radial) .on('contextmenu', (event, d) => { event.preventDefault(); navigateToItem(d.data.id, type); // Navigate to the item in the sidebar }); // Labels (without emoji) node.append('text') .attr('dy', '0.31em') .attr('x', +nodeSize / 2 + 5) .attr('text-anchor', 'center') // Always center the text .attr('transform', d => `rotate(0)`) // No flipping of text .text(d => d.data.name) // Do not include emoji in label .style('font-size', `${labelFontSize}px`) .style('fill', 'var(--text-color)') .on('mouseover', (event, d) => { const content = type === 'character' ? getCharacterTooltipContent(d.data, true) : getLocationTooltipContent(d.data, true); showTooltip_radial(event, content); }) .on('mousemove', (event) => { moveTooltip_radial(event); }) .on('mouseout', hideTooltip_radial) .on('contextmenu', (event, d) => { event.preventDefault(); navigateToItem(d.data.id, type); // Navigate to the item in the sidebar }); // --- Rotation Knob UI --- const knobContainer = document.createElement('div'); knobContainer.style.cssText = ` position: absolute; bottom: 10px; right: 10px; width: 100px; height: 100px; z-index: 10003; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; `; const knob = document.createElement('div'); knob.style.cssText = ` width: 60px; height: 60px; background-color: var(--button-bg-color); border-radius: 50%; position: relative; transform: rotate(0deg); /* Initial rotation state */ `; // Add a small indicator inside the knob to show the starting point const knobIndicator = document.createElement('div'); knobIndicator.style.cssText = ` position: absolute; top: 5px; left: 50%; transform: translateX(-50%); width: 10px; height: 10px; background-color: var(--text-color); border-radius: 50%; `; knob.appendChild(knobIndicator); knobContainer.appendChild(knob); container.appendChild(knobContainer); let isDragging = false; let previousAngle = 0; // Utility function to get the angle of the mouse relative to the center of the knob function getAngleFromEvent(event, element) { const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const deltaX = event.clientX - centerX; const deltaY = event.clientY - centerY; const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); return angle; } // Function to start dragging the knob knob.addEventListener('mousedown', (event) => { isDragging = true; previousAngle = getAngleFromEvent(event, knob); event.preventDefault(); }); // Function to handle dragging document.addEventListener('mousemove', (event) => { if (isDragging) { const currentAngle = getAngleFromEvent(event, knob); const deltaAngle = currentAngle - previousAngle; // Update the current rotation currentRotation = (currentRotation + deltaAngle) % 360; if (currentRotation < 0) currentRotation += 360; // Ensure positive rotation // Apply the combined transformation (rotation, zoom, and pan) svg.attr('transform', `translate(${currentTransform.x},${currentTransform.y}) scale(${currentTransform.k}) rotate(${currentRotation})`); // Rotate the knob visually knob.style.transform = `rotate(${currentRotation}deg)`; // Update previous angle for the next move previousAngle = currentAngle; } }); // Stop dragging when mouse is released document.addEventListener('mouseup', () => { isDragging = false; }); // --- Handle window close --- const closeButton = container.querySelector('button.close-radial-tree'); // Assuming you have a close button if (closeButton) { closeButton.addEventListener('click', () => { // Reset currentTransform to its default values when the window is closed currentTransform = { x: width / 2, y: height / 2, k: 1 }; currentRotation = 0; // Reset rotation as well // Reset the zoom behavior to the default state d3.select(container).select('svg').call(zoomBehavior.transform, d3.zoomIdentity.translate(width / 2, height / 2)); }); } // Resize listener window.addEventListener('resize', () => { svg.attr('width', container.clientWidth).attr('height', container.clientHeight); }); } function navigateToItem(id, type) { if (type === 'character') { // Expand groups and scroll to the character characterGroups.forEach(group => { expandItemAndFind(group, id, type); }); renderCharacterGroups(); const itemElement = document.querySelector(`#character-group-list [data-id="${id}"]`); if (itemElement) { // Ensure the character menu stays open if (characterSidebar.style.right === '-350px') { characterToggleButton.click(); // Open the sidebar if it's closed } itemElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } else { // Expand groups and scroll to the location locationGroups.forEach(group => { expandItemAndFind(group, id, type); }); renderLocationGroups(); const itemElement = document.querySelector(`#location-group-list [data-id="${id}"]`); if (itemElement) { // Ensure the location menu stays open if (locationSidebar.style.left === '-350px') { locationToggleButton.click(); // Open the sidebar if it's closed } itemElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } } // Function to expand groups and find item (recursive) function expandItemAndFind(item, id, type) { if (item.id === id) { return true; } let found = false; if (item.items && item.items.length > 0) { for (let i = 0; i < item.items.length; i++) { if (expandItemAndFind(item.items[i], id, type)) { item.collapsed = false; // Expand parent found = true; } } } if (item.children && item.children.length > 0) { for (let i = 0; i < item.children.length; i++) { if (expandItemAndFind(item.children[i], id, type)) { item.collapsed = false; // Expand parent found = true; } } } return found; } // Function to create structured data for D3.js function structureData(groups) { return groups.map(group => ({ name: group.name, emoji: '📁', // Assign folder emoji to groups id: group.id || generateId(), children: group.items.map(item => mapItem(item)) })); } function mapItem(item) { return { id: item.id, name: item.name, emoji: item.emoji || '', // Use item's emoji, empty string if none description: item.description || item.bio || '', children: item.children ? item.children.map(child => mapItem(child)) : [] }; } // Tooltips for Radial Tree let radialTooltipElement = null; function showTooltip_radial(event, content) { hideTooltip_radial(); // Remove existing tooltip if any // Create new tooltip element radialTooltipElement = document.createElement('div'); radialTooltipElement.style.cssText = ` position: absolute; z-index: 100003; background-color: var(--bg-tool); color: var(--text-color); border: 1px solid var(--border-color); padding: 5px; border-radius: 5px; font-size: 12px; max-width: 600px; white-space: pre-wrap; box-shadow: 0 0 10px var(--shadow-color); pointer-events: none; `; radialTooltipElement.innerHTML = content; radialTooltipElement.style.left = `${event.pageX + 10}px`; radialTooltipElement.style.top = `${event.pageY + 10}px`; document.body.appendChild(radialTooltipElement); } function moveTooltip_radial(event) { if (radialTooltipElement) { radialTooltipElement.style.left = `${event.pageX + 10}px`; radialTooltipElement.style.top = `${event.pageY + 10}px`; } } function hideTooltip_radial() { if (radialTooltipElement && radialTooltipElement.parentNode) { radialTooltipElement.parentNode.removeChild(radialTooltipElement); radialTooltipElement = null; // Set to null after removing } } // Adding buttons to headers // Character Header Modifications characterHeader.innerHTML = ''; const characterHeaderContainer = document.createElement('div'); characterHeaderContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; `; // Search Button for Characters (Left Side) const characterSearchButton = document.createElement('button'); characterSearchButton.textContent = '🔍'; characterSearchButton.title = 'Open Search Panel'; characterSearchButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; font-size: 18px; margin-right: 10px; `; characterHeaderContainer.appendChild(characterSearchButton); // Character Title const characterTitle = document.createElement('span'); characterTitle.textContent = 'Characters'; characterTitle.style.cssText = ` color: var(--text-color); flex-grow: 1; text-align: center; `; characterHeaderContainer.appendChild(characterTitle); // Radial Tree Button for Characters (Right Side) const characterRadialButton = document.createElement('button'); characterRadialButton.textContent = '🌐'; characterRadialButton.title = 'Open Radial Tree'; characterRadialButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; font-size: 18px; margin-left: 10px; `; characterHeaderContainer.appendChild(characterRadialButton); characterHeader.appendChild(characterHeaderContainer); // Location Header Modifications locationHeader.innerHTML = ''; const locationHeaderContainer = document.createElement('div'); locationHeaderContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; `; // Radial Tree Button for Locations (Left Side) const locationRadialButton = document.createElement('button'); locationRadialButton.textContent = '🌐'; locationRadialButton.title = 'Open Radial Tree'; locationRadialButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; font-size: 18px; margin-right: 10px; `; locationHeaderContainer.appendChild(locationRadialButton); // Location Title const locationTitle = document.createElement('span'); locationTitle.textContent = 'Locations'; locationTitle.style.cssText = ` color: var(--text-color); flex-grow: 1; text-align: center; `; locationHeaderContainer.appendChild(locationTitle); // Search Button for Locations (Right Side) const locationSearchButton = document.createElement('button'); locationSearchButton.textContent = '🔍'; locationSearchButton.title = 'Open Search Panel'; locationSearchButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; font-size: 18px; margin-left: 10px; `; locationHeaderContainer.appendChild(locationSearchButton); locationHeader.appendChild(locationHeaderContainer); // Function to open search panel (Integrated into Sidebar) function toggleSearchPanel(type) { if (type === 'character') { if (searchPanelOpen.character) { // Close search panel characterSearchContainer.style.display = 'none'; searchPanelOpen.character = false; } else { // Open search panel characterSearchContainer.style.display = 'block'; searchPanelOpen.character = true; characterSearchInput.focus(); } } else { if (searchPanelOpen.location) { // Close search panel locationSearchContainer.style.display = 'none'; searchPanelOpen.location = false; } else { // Open search panel locationSearchContainer.style.display = 'block'; searchPanelOpen.location = true; locationSearchInput.focus(); } } } // Character Search Panel const characterSearchContainer = document.createElement('div'); characterSearchContainer.style.cssText = ` position: relative; display: none; padding: 10px; background-color: var(--bg-color); `; const characterSearchInput = document.createElement('input'); characterSearchInput.type = 'text'; characterSearchInput.placeholder = 'Search...'; characterSearchInput.style.cssText = ` width: 100%; margin-bottom: 0px; padding: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); `; // Adjust the results container to match the search input width const characterResultsContainer = document.createElement('div'); characterResultsContainer.style.cssText = ` position: absolute; top: calc(100% + 5px); /* Position below the search input */ left: 0; width: 100%; max-height: 200px; overflow-y: auto; background-color: var(--bg-color); border: 1px solid var(--border-color); box-shadow: 0 0 10px var(--shadow-color); z-index: 1000; `; characterSearchContainer.appendChild(characterSearchInput); characterSearchContainer.appendChild(characterResultsContainer); characterSidebar.insertBefore(characterSearchContainer, characterGroupList); characterSearchInput.addEventListener('input', () => { const query = characterSearchInput.value.trim().toLowerCase(); characterResultsContainer.innerHTML = ''; if (query.length > 0) { const results = searchItems(query, 'character'); if (results.length > 0) { results.forEach(result => { const resultItem = document.createElement('div'); resultItem.style.cssText = ` padding: 5px; border-bottom: 1px solid var(--border-color); cursor: pointer; color: var(--text-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; resultItem.addEventListener('click', () => { navigateToItem(result.id, 'character'); characterSearchInput.value = ''; characterResultsContainer.innerHTML = ''; }); resultItem.innerHTML = formatSearchResult(result); characterResultsContainer.appendChild(resultItem); }); } else { characterResultsContainer.textContent = 'No results found.'; } } }); // Location Search Panel const locationSearchContainer = document.createElement('div'); locationSearchContainer.style.cssText = ` position: relative; display: none; padding: 10px; background-color: var(--bg-color); `; const locationSearchInput = document.createElement('input'); locationSearchInput.type = 'text'; locationSearchInput.placeholder = 'Search...'; locationSearchInput.style.cssText = ` width: 100%; margin-bottom: 0px; padding: 5px; border: 1px solid var(--border-color); border-radius: 5px; background-color: var(--bg-color); color: var(--text-color); `; // Adjust the results container to match the search input width const locationResultsContainer = document.createElement('div'); locationResultsContainer.style.cssText = ` position: absolute; top: calc(100% + 5px); /* Position below the search input */ left: 0; width: 100%; max-height: 200px; overflow-y: auto; background-color: var(--bg-color); border: 1px solid var(--border-color); box-shadow: 0 0 10px var(--shadow-color); z-index: 1000; `; locationSearchContainer.appendChild(locationSearchInput); locationSearchContainer.appendChild(locationResultsContainer); locationSidebar.insertBefore(locationSearchContainer, locationGroupList); locationSearchInput.addEventListener('input', () => { const query = locationSearchInput.value.trim().toLowerCase(); locationResultsContainer.innerHTML = ''; if (query.length > 0) { const results = searchItems(query, 'location'); if (results.length > 0) { results.forEach(result => { const resultItem = document.createElement('div'); resultItem.style.cssText = ` padding: 5px; border-bottom: 1px solid var(--border-color); cursor: pointer; color: var(--text-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; resultItem.addEventListener('click', () => { navigateToItem(result.id, 'location'); locationSearchInput.value = ''; locationResultsContainer.innerHTML = ''; }); resultItem.innerHTML = formatSearchResult(result); locationResultsContainer.appendChild(resultItem); }); } else { locationResultsContainer.textContent = 'No results found.'; } } }); locationSearchButton.addEventListener('click', () => { toggleSearchPanel('location'); }); // Radial Tree Button Event Listeners characterRadialButton.addEventListener('click', () => { if (radialTreeOpen) { document.getElementById('radial-tree-window').remove(); radialTreeOpen = false; } const data = structureData(characterGroups); createRadialTree(data, 'character'); }); locationRadialButton.addEventListener('click', () => { if (radialTreeOpen) { document.getElementById('radial-tree-window').remove(); radialTreeOpen = false; } const data = structureData(locationGroups); createRadialTree(data, 'location'); }); // Function to search items function searchItems(query, type) { const results = []; const groups = type === 'character' ? characterGroups : locationGroups; groups.forEach(group => { searchGroup(group, query, results, []); }); return results; } function searchGroup(group, query, results, path) { const newPath = [...path, group.name]; group.items.forEach(item => { searchItem(item, query, results, newPath); }); } function searchItem(item, query, results, path) { const newPath = [...path, item.name]; if (item.name.toLowerCase().includes(query)) { results.push({ id: item.id, path: newPath }); } if (item.children && item.children.length > 0) { item.children.forEach(child => { searchItem(child, query, results, newPath); }); } } function formatSearchResult(result) { let html = ''; const maxCharacters = 42; // Adjust as needed let displayPath = result.path.join(' > '); if (displayPath.length > maxCharacters) { displayPath = '...' + displayPath.slice(-maxCharacters); } const pathSegments = displayPath.split(' > '); for (let i = 0; i < pathSegments.length - 1; i++) { html += `<span style="font-size: 12px; color: var(--text-color-darker); white-space: nowrap;">${pathSegments[i]} > </span>`; } html += `<strong style="white-space: nowrap;">${pathSegments[pathSegments.length - 1]}</strong>`; return `<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${html}</div>`; } })();