您需要先安装一个扩展,例如 篡改猴、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 5.5 // @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'; let c_radius = 300; let c_labelFontSize = 12; let c_nodeSize = 24; let l_radius = 300; let l_labelFontSize = 12; let l_nodeSize = 24; let customContextMenu; let initialMouseX; let initialMouseY; const hideDistance = 100; // Distance in pixels to hide the menu let ItemTransferTarget = null; //.addEventListener('contextmenu', showMenu); function CreateContextMenu(mainColor, textColor, optionNames, optionFunctions) { // Destroy existing menu if present DestroyContextMenu(); // Create a new custom menu customContextMenu = document.createElement('div'); customContextMenu.style.position = 'absolute'; customContextMenu.style.background = mainColor; customContextMenu.style.color = textColor; customContextMenu.style.border = '1px solid #ccc'; // Adjusted padding for 2/3 size customContextMenu.style.padding = '3.33px 6.67px'; customContextMenu.style.display = 'none'; customContextMenu.style.zIndex = '99999999'; // High z-index // Add menu items with dividers optionNames.forEach((name, index) => { const menuItem = document.createElement('div'); // Adjusted padding and font size for 2/3 size menuItem.style.padding = '3.33px 0'; menuItem.style.cursor = 'pointer'; menuItem.style.fontSize = '0.67em'; menuItem.textContent = name; menuItem.addEventListener('click', () => { optionFunctions[index](); hideMenu(); }); // Append menu item customContextMenu.appendChild(menuItem); // Add a divider except after the last item if (index < optionNames.length - 1) { const divider = document.createElement('div'); divider.style.borderTop = '1px solid #ccc'; customContextMenu.appendChild(divider); } }); // Append the menu to the body document.body.appendChild(customContextMenu); // Check mouse distance document.addEventListener('mousemove', trackMouseDistance); } function DestroyContextMenu() { if (customContextMenu) { customContextMenu.remove(); document.removeEventListener('contextmenu', showMenu); document.removeEventListener('click', hideMenu); document.removeEventListener('mousemove', trackMouseDistance); customContextMenu = null; } } function showMenu(event) { event.preventDefault(); initialMouseX = event.clientX; initialMouseY = event.clientY; if (customContextMenu) { customContextMenu.style.top = `${event.clientY}px`; customContextMenu.style.left = `${event.clientX}px`; customContextMenu.style.display = 'block'; } } function hideMenu() { if (customContextMenu) { customContextMenu.style.display = 'none'; } } function trackMouseDistance(event) { if (customContextMenu && customContextMenu.style.display === 'block') { const distance = Math.sqrt( Math.pow(event.clientX - initialMouseX, 2) + Math.pow(event.clientY - initialMouseY, 2) ); if (distance > hideDistance && !customContextMenu.contains(event.target)) { hideMenu(); } } } // Define Themes const themes = { // Default Dark Theme dark: { '--bg-color': 'rgba(34, 34, 34, var(--ui-transparency))', '--bg-color-full': 'rgba(34, 34, 34, 1)', '--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' }, // Default Light Theme light: { '--bg-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 255, 255, 1)', '--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 Themes sepia_light: { '--bg-color': 'rgba(244, 232, 208, var(--ui-transparency))', '--bg-color-full': 'rgba(244, 232, 208, 1)', '--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-color-full': 'rgba(60, 45, 31, 1)', '--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 Themes solarized_light: { '--bg-color': 'rgba(253, 246, 227, var(--ui-transparency))', '--bg-color-full': 'rgba(253, 246, 227, 1)', '--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-color-full': 'rgba(0, 43, 54, 1)', '--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 Themes (Green) forest_light: { '--bg-color': 'rgba(233, 245, 233, var(--ui-transparency))', '--bg-color-full': 'rgba(233, 245, 233, 1)', '--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-color-full': 'rgba(34, 49, 34, 1)', '--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 Themes (Blue) ocean_light: { '--bg-color': 'rgba(224, 244, 252, var(--ui-transparency))', '--bg-color-full': 'rgba(224, 244, 252, 1)', '--bg-tool': 'rgba(224, 244, 252, 0.8)', '--text-color': '#004766', '--text-color-darker': '#00334c', '--border-color': 'rgba(204, 232, 245, 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.2)', '--link-color': '#0077b6', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#ff7f50' }, ocean_dark: { '--bg-color': 'rgba(0, 30, 60, var(--ui-transparency))', '--bg-color-full': 'rgba(0, 30, 60, 1)', '--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' }, // Sunset Themes (Red/Orange) sunset_light: { '--bg-color': 'rgba(255, 237, 219, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 237, 219, 1)', '--bg-tool': 'rgba(255, 237, 219, 0.8)', '--text-color': '#5d1a1a', '--text-color-darker': '#3b0f0f', '--border-color': 'rgba(255, 214, 170, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 87, 34, var(--ui-transparency))', '--active-char-color': 'rgba(255, 152, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(255, 179, 71, var(--ui-transparency))', '--info-bg-color': 'rgba(255, 138, 101, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 112, 67, var(--ui-transparency))', '--muted-bg-color': 'rgba(255, 224, 178, var(--ui-transparency))', '--danger-bg-color': 'rgba(183, 28, 28, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#e65100', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#d50000' }, sunset_dark: { '--bg-color': 'rgba(66, 28, 82, var(--ui-transparency))', '--bg-color-full': 'rgba(66, 28, 82, 1)', '--bg-tool': 'rgba(66, 28, 82, 0.8)', '--text-color': '#fce4ec', '--text-color-darker': '#f8bbd0', '--border-color': 'rgba(127, 63, 152, var(--ui-transparency))', '--button-bg-color': 'rgba(233, 30, 99, var(--ui-transparency))', '--active-char-color': 'rgba(255, 64, 129, var(--ui-transparency))', '--success-bg-color': 'rgba(186, 104, 200, var(--ui-transparency))', '--info-bg-color': 'rgba(142, 36, 170, var(--ui-transparency))', '--warning-bg-color': 'rgba(216, 27, 96, var(--ui-transparency))', '--muted-bg-color': 'rgba(123, 31, 162, var(--ui-transparency))', '--danger-bg-color': 'rgba(74, 20, 140, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.4)', '--link-color': '#d81b60', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#f50057' }, // Sunshine Themes (Yellow) sunshine_light: { '--bg-color': 'rgba(255, 249, 196, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 249, 196, 1)', '--bg-tool': 'rgba(255, 249, 196, 0.8)', '--text-color': '#795548', '--text-color-darker': '#5d4037', '--border-color': 'rgba(255, 241, 118, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 235, 59, var(--ui-transparency))', '--active-char-color': 'rgba(255, 179, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(253, 216, 53, var(--ui-transparency))', '--info-bg-color': 'rgba(255, 202, 40, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 193, 7, var(--ui-transparency))', '--muted-bg-color': 'rgba(255, 224, 130, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 152, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#ffab00', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#ff6f00' }, sunshine_dark: { '--bg-color': 'rgba(50, 50, 0, var(--ui-transparency))', '--bg-color-full': 'rgba(50, 50, 0, 1)', '--bg-tool': 'rgba(50, 50, 0, 0.8)', '--text-color': '#fff9c4', '--text-color-darker': '#fff59d', '--border-color': 'rgba(85, 85, 0, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 214, 0, var(--ui-transparency))', '--active-char-color': 'rgba(255, 171, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(255, 238, 88, var(--ui-transparency))', '--info-bg-color': 'rgba(255, 235, 59, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 193, 7, var(--ui-transparency))', '--muted-bg-color': 'rgba(212, 175, 55, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 111, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.4)', '--link-color': '#ffab00', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#ff6f00' }, // Twilight Themes (Indigo/Violet) twilight_light: { '--bg-color': 'rgba(230, 230, 250, var(--ui-transparency))', '--bg-color-full': 'rgba(230, 230, 250, 1)', '--bg-tool': 'rgba(230, 230, 250, 0.8)', '--text-color': '#4b0082', '--text-color-darker': '#2e0047', '--border-color': 'rgba(216, 191, 216, var(--ui-transparency))', '--button-bg-color': 'rgba(75, 0, 130, var(--ui-transparency))', '--active-char-color': 'rgba(138, 43, 226, var(--ui-transparency))', '--success-bg-color': 'rgba(111, 0, 255, var(--ui-transparency))', '--info-bg-color': 'rgba(153, 50, 204, var(--ui-transparency))', '--warning-bg-color': 'rgba(186, 85, 211, var(--ui-transparency))', '--muted-bg-color': 'rgba(148, 0, 211, var(--ui-transparency))', '--danger-bg-color': 'rgba(199, 21, 133, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#8a2be2', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#9400d3' }, twilight_dark: { '--bg-color': 'rgba(18, 10, 30, var(--ui-transparency))', '--bg-color-full': 'rgba(18, 10, 30, 1)', '--bg-tool': 'rgba(18, 10, 30, 0.8)', '--text-color': '#d8bfd8', '--text-color-darker': '#dda0dd', '--border-color': 'rgba(49, 24, 73, var(--ui-transparency))', '--button-bg-color': 'rgba(138, 43, 226, var(--ui-transparency))', '--active-char-color': 'rgba(153, 50, 204, var(--ui-transparency))', '--success-bg-color': 'rgba(186, 85, 211, var(--ui-transparency))', '--info-bg-color': 'rgba(148, 0, 211, var(--ui-transparency))', '--warning-bg-color': 'rgba(221, 160, 221, var(--ui-transparency))', '--muted-bg-color': 'rgba(123, 104, 238, var(--ui-transparency))', '--danger-bg-color': 'rgba(199, 21, 133, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.4)', '--link-color': '#ba55d3', '--code-bg-color': 'rgba(0, 0, 0, 0.3)', '--code-text-color': '#ee82ee' }, // Terminal Themes terminal_light: { '--bg-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 255, 255, 1)', '--bg-tool': 'rgba(255, 255, 255, 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.1)', '--code-text-color': '#7fff00' }, terminal_dark: { '--bg-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--bg-color-full': 'rgba(0, 0, 0, 1)', '--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 Themes retro_light: { '--bg-color': 'rgba(196, 182, 187, var(--ui-transparency))', '--bg-color-full': 'rgba(196, 182, 187, 1)', '--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' }, retro_dark: { '--bg-color': 'rgba(34, 32, 52, var(--ui-transparency))', '--bg-color-full': 'rgba(34, 32, 52, 1)', '--bg-tool': 'rgba(34, 32, 52, 0.8)', '--text-color': '#c2c3c7', '--text-color-darker': '#8b8d90', '--border-color': 'rgba(69, 40, 60, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 0, 77, var(--ui-transparency))', '--active-char-color': 'rgba(255, 163, 0, var(--ui-transparency))', '--success-bg-color': 'rgba(0, 232, 216, var(--ui-transparency))', '--info-bg-color': 'rgba(44, 232, 245, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 236, 39, var(--ui-transparency))', '--muted-bg-color': 'rgba(96, 86, 107, var(--ui-transparency))', '--danger-bg-color': 'rgba(172, 50, 50, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#ff004d', '--code-bg-color': 'rgba(69, 40, 60, 0.8)', '--code-text-color': '#ff77a8' }, // Neon Themes neon_light: { '--bg-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 255, 255, 1)', '--bg-tool': 'rgba(255, 255, 255, 0.8)', '--text-color': '#000000', '--text-color-darker': '#333333', '--border-color': 'rgba(0, 0, 0, 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(128, 128, 128, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.5)', '--link-color': '#ff00ff', '--code-bg-color': 'rgba(255, 255, 255, 0.8)', '--code-text-color': '#00ffff' }, neon_dark: { '--bg-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--bg-color-full': 'rgba(0, 0, 0, 1)', '--bg-tool': 'rgba(0, 0, 0, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--border-color': 'rgba(255, 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 Themes vintage_light: { '--bg-color': 'rgba(240, 230, 140, var(--ui-transparency))', '--bg-color-full': 'rgba(240, 230, 140, 1)', '--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' }, vintage_dark: { '--bg-color': 'rgba(60, 47, 34, var(--ui-transparency))', '--bg-color-full': 'rgba(60, 47, 34, 1)', '--bg-tool': 'rgba(60, 47, 34, 0.8)', '--text-color': '#e0d4b3', '--text-color-darker': '#c0b297', '--border-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--button-bg-color': 'rgba(128, 0, 0, var(--ui-transparency))', '--active-char-color': 'rgba(218, 165, 32, var(--ui-transparency))', '--success-bg-color': 'rgba(107, 142, 35, var(--ui-transparency))', '--info-bg-color': 'rgba(70, 130, 180, var(--ui-transparency))', '--warning-bg-color': 'rgba(184, 134, 11, 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(0,0,0,0.5)', '--link-color': '#8b0000', '--code-bg-color': 'rgba(245, 222, 179, 0.3)', '--code-text-color': '#ffa07a' }, // Pastel Themes pastel_light: { '--bg-color': 'rgba(255, 228, 225, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 228, 225, 1)', '--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' }, pastel_dark: { '--bg-color': 'rgba(75, 75, 75, var(--ui-transparency))', '--bg-color-full': 'rgba(75, 75, 75, 1)', '--bg-tool': 'rgba(75, 75, 75, 0.8)', '--text-color': '#e6e6e6', '--text-color-darker': '#cccccc', '--border-color': 'rgba(105, 105, 105, var(--ui-transparency))', '--button-bg-color': 'rgba(176, 196, 222, var(--ui-transparency))', '--active-char-color': 'rgba(119, 136, 153, var(--ui-transparency))', '--success-bg-color': 'rgba(144, 238, 144, var(--ui-transparency))', '--info-bg-color': 'rgba(135, 206, 250, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 160, 122, var(--ui-transparency))', '--muted-bg-color': 'rgba(205, 133, 63, var(--ui-transparency))', '--danger-bg-color': 'rgba(205, 92, 92, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#ff69b4', '--code-bg-color': 'rgba(255, 182, 193, 0.3)', '--code-text-color': '#dc143c' }, // Monokai Themes monokai_light: { '--bg-color': 'rgba(248, 248, 242, var(--ui-transparency))', '--bg-color-full': 'rgba(248, 248, 242, 1)', '--bg-tool': 'rgba(248, 248, 242, 0.8)', '--text-color': '#272822', '--text-color-darker': '#49483e', '--border-color': 'rgba(220, 220, 217, var(--ui-transparency))', '--button-bg-color': 'rgba(249, 38, 114, var(--ui-transparency))', '--active-char-color': 'rgba(166, 226, 46, var(--ui-transparency))', '--success-bg-color': 'rgba(166, 226, 46, var(--ui-transparency))', '--info-bg-color': 'rgba(102, 217, 239, var(--ui-transparency))', '--warning-bg-color': 'rgba(253, 151, 31, var(--ui-transparency))', '--muted-bg-color': 'rgba(117, 113, 94, var(--ui-transparency))', '--danger-bg-color': 'rgba(249, 38, 114, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.2)', '--link-color': '#ae81ff', '--code-bg-color': 'rgba(230, 230, 230, 0.8)', '--code-text-color': '#f92672' }, monokai_dark: { '--bg-color': 'rgba(39, 40, 34, var(--ui-transparency))', '--bg-color-full': 'rgba(39, 40, 34, 1)', '--bg-tool': 'rgba(39, 40, 34, 0.8)', '--text-color': '#f8f8f2', '--text-color-darker': '#e6e6dc', '--border-color': 'rgba(73, 72, 62, var(--ui-transparency))', '--button-bg-color': 'rgba(249, 38, 114, var(--ui-transparency))', '--active-char-color': 'rgba(166, 226, 46, var(--ui-transparency))', '--success-bg-color': 'rgba(102, 217, 239, var(--ui-transparency))', '--info-bg-color': 'rgba(102, 217, 239, var(--ui-transparency))', '--warning-bg-color': 'rgba(253, 151, 31, var(--ui-transparency))', '--muted-bg-color': 'rgba(117, 113, 94, var(--ui-transparency))', '--danger-bg-color': 'rgba(249, 38, 114, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#ae81ff', '--code-bg-color': 'rgba(39, 40, 34, 0.8)', '--code-text-color': '#f92672' }, // Dracula Themes dracula_light: { '--bg-color': 'rgba(248, 248, 242, var(--ui-transparency))', '--bg-color-full': 'rgba(248, 248, 242, 1)', '--bg-tool': 'rgba(248, 248, 242, 0.8)', '--text-color': '#282a36', '--text-color-darker': '#44475a', '--border-color': 'rgba(211, 212, 219, var(--ui-transparency))', '--button-bg-color': 'rgba(98, 114, 164, var(--ui-transparency))', '--active-char-color': 'rgba(189, 147, 249, var(--ui-transparency))', '--success-bg-color': 'rgba(80, 250, 123, var(--ui-transparency))', '--info-bg-color': 'rgba(139, 233, 253, var(--ui-transparency))', '--warning-bg-color': 'rgba(241, 250, 140, var(--ui-transparency))', '--muted-bg-color': 'rgba(98, 114, 164, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 85, 85, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.2)', '--link-color': '#6272a4', '--code-bg-color': 'rgba(225, 225, 232, 0.8)', '--code-text-color': '#ff79c6' }, dracula_dark: { '--bg-color': 'rgba(40, 42, 54, var(--ui-transparency))', '--bg-color-full': 'rgba(40, 42, 54, 1)', '--bg-tool': 'rgba(40, 42, 54, 0.8)', '--text-color': '#f8f8f2', '--text-color-darker': '#e6e6dc', '--border-color': 'rgba(68, 71, 90, var(--ui-transparency))', '--button-bg-color': 'rgba(98, 114, 164, var(--ui-transparency))', '--active-char-color': 'rgba(189, 147, 249, var(--ui-transparency))', '--success-bg-color': 'rgba(80, 250, 123, var(--ui-transparency))', '--info-bg-color': 'rgba(139, 233, 253, var(--ui-transparency))', '--warning-bg-color': 'rgba(241, 250, 140, var(--ui-transparency))', '--muted-bg-color': 'rgba(98, 114, 164, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 85, 85, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#6272a4', '--code-bg-color': 'rgba(68, 71, 90, 0.8)', '--code-text-color': '#ff79c6' }, // Gruvbox Themes gruvbox_light: { '--bg-color': 'rgba(251, 241, 199, var(--ui-transparency))', '--bg-color-full': 'rgba(251, 241, 199, 1)', '--bg-tool': 'rgba(251, 241, 199, 0.8)', '--text-color': '#282828', '--text-color-darker': '#1d2021', '--border-color': 'rgba(213, 196, 161, var(--ui-transparency))', '--button-bg-color': 'rgba(184, 187, 38, var(--ui-transparency))', '--active-char-color': 'rgba(184, 187, 38, var(--ui-transparency))', '--success-bg-color': 'rgba(121, 116, 14, var(--ui-transparency))', '--info-bg-color': 'rgba(7, 102, 120, var(--ui-transparency))', '--warning-bg-color': 'rgba(215, 153, 33, var(--ui-transparency))', '--muted-bg-color': 'rgba(146, 131, 116, var(--ui-transparency))', '--danger-bg-color': 'rgba(204, 36, 29, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.2)', '--link-color': '#458588', '--code-bg-color': 'rgba(235, 219, 178, 0.8)', '--code-text-color': '#9d0006' }, gruvbox_dark: { '--bg-color': 'rgba(40, 40, 40, var(--ui-transparency))', '--bg-color-full': 'rgba(40, 40, 40, 1)', '--bg-tool': 'rgba(40, 40, 40, 0.8)', '--text-color': '#ebdbb2', '--text-color-darker': '#d5c4a1', '--border-color': 'rgba(60, 56, 54, var(--ui-transparency))', '--button-bg-color': 'rgba(213, 196, 161, var(--ui-transparency))', '--active-char-color': 'rgba(184, 187, 38, var(--ui-transparency))', '--success-bg-color': 'rgba(121, 116, 14, var(--ui-transparency))', '--info-bg-color': 'rgba(7, 102, 120, var(--ui-transparency))', '--warning-bg-color': 'rgba(215, 153, 33, var(--ui-transparency))', '--muted-bg-color': 'rgba(146, 131, 116, var(--ui-transparency))', '--danger-bg-color': 'rgba(204, 36, 29, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.4)', '--link-color': '#83a598', '--code-bg-color': 'rgba(60, 56, 54, 0.8)', '--code-text-color': '#fb4934' }, // Nord Themes nord_light: { '--bg-color': 'rgba(236, 239, 244, var(--ui-transparency))', '--bg-color-full': 'rgba(236, 239, 244, 1)', '--bg-tool': 'rgba(236, 239, 244, 0.8)', '--text-color': '#2e3440', '--text-color-darker': '#3b4252', '--border-color': 'rgba(208, 211, 216, var(--ui-transparency))', '--button-bg-color': 'rgba(94, 129, 172, var(--ui-transparency))', '--active-char-color': 'rgba(136, 192, 208, var(--ui-transparency))', '--success-bg-color': 'rgba(163, 190, 140, var(--ui-transparency))', '--info-bg-color': 'rgba(129, 161, 193, var(--ui-transparency))', '--warning-bg-color': 'rgba(235, 203, 139, var(--ui-transparency))', '--muted-bg-color': 'rgba(186, 190, 194, var(--ui-transparency))', '--danger-bg-color': 'rgba(191, 97, 106, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.2)', '--link-color': '#5e81ac', '--code-bg-color': 'rgba(208, 211, 216, 0.8)', '--code-text-color': '#bf616a' }, nord_dark: { '--bg-color': 'rgba(46, 52, 64, var(--ui-transparency))', '--bg-color-full': 'rgba(46, 52, 64, 1)', '--bg-tool': 'rgba(46, 52, 64, 0.8)', '--text-color': '#d8dee9', '--text-color-darker': '#eceff4', '--border-color': 'rgba(59, 66, 82, var(--ui-transparency))', '--button-bg-color': 'rgba(94, 129, 172, var(--ui-transparency))', '--active-char-color': 'rgba(136, 192, 208, var(--ui-transparency))', '--success-bg-color': 'rgba(163, 190, 140, var(--ui-transparency))', '--info-bg-color': 'rgba(129, 161, 193, var(--ui-transparency))', '--warning-bg-color': 'rgba(235, 203, 139, var(--ui-transparency))', '--muted-bg-color': 'rgba(76, 86, 106, var(--ui-transparency))', '--danger-bg-color': 'rgba(191, 97, 106, var(--ui-transparency))', '--shadow-color': 'rgba(0,0,0,0.5)', '--link-color': '#88c0d0', '--code-bg-color': 'rgba(59, 66, 82, 0.8)', '--code-text-color': '#bf616a' }, // High Contrast Themes high_contrast_light: { '--bg-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--bg-color-full': 'rgba(255, 255, 255, 1)', '--bg-tool': 'rgba(255, 255, 255, 0.8)', '--text-color': '#000000', '--text-color-darker': '#333333', '--border-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 255, 0, var(--ui-transparency))', '--active-char-color': 'rgba(255, 0, 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(128, 128, 128, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.5)', '--link-color': '#0000ff', '--code-bg-color': 'rgba(0, 0, 0, 0.1)', '--code-text-color': '#0000ff' }, high_contrast_dark: { '--bg-color': 'rgba(0, 0, 0, var(--ui-transparency))', '--bg-color-full': 'rgba(0, 0, 0, 1)', '--bg-tool': 'rgba(0, 0, 0, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#cccccc', '--border-color': 'rgba(255, 255, 255, var(--ui-transparency))', '--button-bg-color': 'rgba(255, 255, 0, var(--ui-transparency))', '--active-char-color': 'rgba(255, 0, 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(128, 128, 128, var(--ui-transparency))', '--danger-bg-color': 'rgba(255, 0, 0, var(--ui-transparency))', '--shadow-color': 'rgba(255, 255, 255, 0.5)', '--link-color': '#00ffff', '--code-bg-color': 'rgba(255, 255, 255, 0.1)', '--code-text-color': '#ffff00' }, // Material Design Themes material_light: { '--bg-color': 'rgba(250, 250, 250, var(--ui-transparency))', '--bg-color-full': 'rgba(250, 250, 250, 1)', '--bg-tool': 'rgba(250, 250, 250, 0.8)', '--text-color': '#212121', '--text-color-darker': '#000000', '--border-color': 'rgba(224, 224, 224, var(--ui-transparency))', '--button-bg-color': 'rgba(33, 150, 243, var(--ui-transparency))', '--active-char-color': 'rgba(0, 188, 212, var(--ui-transparency))', '--success-bg-color': 'rgba(76, 175, 80, var(--ui-transparency))', '--info-bg-color': 'rgba(3, 169, 244, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 152, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(158, 158, 158, var(--ui-transparency))', '--danger-bg-color': 'rgba(244, 67, 54, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.2)', '--link-color': '#009688', '--code-bg-color': 'rgba(238, 238, 238, .8)', '--code-text-color': '#ff5722' }, material_dark: { '--bg-color': 'rgba(33, 33, 33, var(--ui-transparency))', '--bg-color-full': 'rgba(33, 33, 33, 1)', '--bg-tool': 'rgba(33, 33, 33, 0.8)', '--text-color': '#ffffff', '--text-color-darker': '#bdbdbd', '--border-color': 'rgba(66, 66, 66, var(--ui-transparency))', '--button-bg-color': 'rgba(33, 150, 243, var(--ui-transparency))', '--active-char-color': 'rgba(0, 188, 212, var(--ui-transparency))', '--success-bg-color': 'rgba(76, 175, 80, var(--ui-transparency))', '--info-bg-color': 'rgba(3, 169, 244, var(--ui-transparency))', '--warning-bg-color': 'rgba(255, 152, 0, var(--ui-transparency))', '--muted-bg-color': 'rgba(117, 117, 117, var(--ui-transparency))', '--danger-bg-color': 'rgba(244, 67, 54, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.5)', '--link-color': '#009688', '--code-bg-color': 'rgba(55, 71, 79, 0.8)', '--code-text-color': '#ff5722' }, // Desert Themes desert_light: { '--bg-color': 'rgba(250, 243, 221, var(--ui-transparency))', '--bg-color-full': 'rgba(250, 243, 221, 1)', '--bg-tool': 'rgba(250, 243, 221, 0.8)', '--text-color': '#5e4a1e', '--text-color-darker': '#3e300f', '--border-color': 'rgba(230, 216, 173, var(--ui-transparency))', '--button-bg-color': 'rgba(205, 133, 63, var(--ui-transparency))', '--active-char-color': 'rgba(244, 164, 96, var(--ui-transparency))', '--success-bg-color': 'rgba(218, 165, 32, var(--ui-transparency))', '--info-bg-color': 'rgba(210, 180, 140, var(--ui-transparency))', '--warning-bg-color': 'rgba(184, 134, 11, var(--ui-transparency))', '--muted-bg-color': 'rgba(222, 184, 135, var(--ui-transparency))', '--danger-bg-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.2)', '--link-color': '#cd853f', '--code-bg-color': 'rgba(245, 222, 179, 0.8)', '--code-text-color': '#8b4513' }, desert_dark: { '--bg-color': 'rgba(74, 60, 42, var(--ui-transparency))', '--bg-color-full': 'rgba(74, 60, 42, 1)', '--bg-tool': 'rgba(74, 60, 42, 0.8)', '--text-color': '#f0e68c', '--text-color-darker': '#d2b48c', '--border-color': 'rgba(92, 64, 51, var(--ui-transparency))', '--button-bg-color': 'rgba(205, 133, 63, var(--ui-transparency))', '--active-char-color': 'rgba(244, 164, 96, var(--ui-transparency))', '--success-bg-color': 'rgba(218, 165, 32, var(--ui-transparency))', '--info-bg-color': 'rgba(210, 180, 140, var(--ui-transparency))', '--warning-bg-color': 'rgba(184, 134, 11, var(--ui-transparency))', '--muted-bg-color': 'rgba(210, 105, 30, var(--ui-transparency))', '--danger-bg-color': 'rgba(139, 69, 19, var(--ui-transparency))', '--shadow-color': 'rgba(0, 0, 0, 0.4)', '--link-color': '#cd853f', '--code-bg-color': 'rgba(92, 64, 51, 0.8)', '--code-text-color': '#ffa07a' } }; // 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(); } } // Default randomizer options const defaultRandomizerOptions = { emoji: { 'Smileys': ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣'], 'People': ['😊', '😇', '🙂', '🙃', '😉', '😌', '😍'], // Add more categories if needed }, name: { 'Common': ['Alex', 'Charlie', 'Sam', 'Jessie', 'Max', 'Taylor', 'Jordan', 'Casey', 'Jamie', 'Robin'], 'Fantasy': ['Aragorn', 'Legolas', 'Gandalf', 'Frodo', 'Bilbo', 'Galadriel', 'Sauron'], // Add more categories if needed }, sex: { 'Standard': ['male', 'female', 'other'], // It's possible to have other options }, species: { 'Realistic': ['Human'], 'Fun': ['Human', 'Elf', 'Dwarf', 'Orc', 'Dragon', 'Fairy', 'Vampire', 'Werewolf', 'Alien', 'Robot'], 'Fantasy': ['Human', 'Elf', 'Dwarf', 'Orc'], }, age: { 'All Ages': Array.from({ length: 80 }, (_, i) => i + 1), // ages 1 to 80 'Adult': Array.from({ length: 53 }, (_, i) => i + 18), // ages 18 to 70 'Child': Array.from({ length: 17 }, (_, i) => i + 1), // ages 1 to 17 }, bodyType: { 'Standard': ['Slim', 'Athletic', 'Average', 'Curvy', 'Muscular', 'Stocky', 'Petite'], // Add more categories if needed }, personality: { 'All': ['Cheerful', 'Serious', 'Energetic', 'Calm', 'Shy', 'Outgoing', 'Snarky', 'Kind', 'Brave', 'Cautious'], 'Positive': ['Cheerful', 'Energetic', 'Kind', 'Brave'], 'Negative': ['Serious', 'Shy', 'Snarky', 'Cautious'], }, bio: { 'Standard': [ 'Loves exploring new places.', 'Enjoys reading books and learning new things.', 'Has a passion for music and the arts.', 'A dedicated athlete and fitness enthusiast.', 'An adventurous spirit with a love for travel.', 'Loyal friend who values honesty and integrity.', 'Tech-savvy individual with a knack for gadgets.', 'Creative thinker who enjoys solving problems.', 'Animal lover who spends time volunteering at shelters.', 'An aspiring chef who loves experimenting in the kitchen.' ], // Add more categories if needed }, }; // User's randomizer settings const randomizerSettings = { emoji: { selectedOption: 'Smileys', customList: '' }, name: { selectedOption: 'Common', customList: '' }, sex: { selectedOption: 'Standard', customList: '' }, species: { selectedOption: 'Fun', customList: '' }, age: { selectedOption: 'All Ages', customList: '' }, bodyType: { selectedOption: 'Standard', customList: '' }, personality: { selectedOption: 'All', customList: '' }, bio: { selectedOption: 'Standard', customList: '' }, }; // Function to generate a random character function generateRandomCharacter() { const character = {}; function getRandomItem(field) { let items = []; if (randomizerSettings[field].customList.trim() !== '') { // Use custom list items = randomizerSettings[field].customList.split(/[,\n]+/).map(item => item.trim()).filter(item => item !== ''); // For age, convert to numbers if (field === 'age') { items = items.map(item => parseInt(item, 10)).filter(item => !isNaN(item)); } } else { // Use selected default option const selectedOption = randomizerSettings[field].selectedOption; items = defaultRandomizerOptions[field][selectedOption]; } // Handle cases where items might be undefined or empty if (!items || items.length === 0) { return ''; } // Randomly pick an item return items[Math.floor(Math.random() * items.length)]; } character.emoji = getRandomItem('emoji'); character.name = getRandomItem('name'); character.sex = getRandomItem('sex'); character.species = getRandomItem('species'); character.age = getRandomItem('age'); character.bodyType = getRandomItem('bodyType'); character.personality = getRandomItem('personality'); character.bio = getRandomItem('bio'); // characterType is not randomized return character; } // Function to capitalize first letter function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } // Function to open Randomizer Settings dialog function openRandomizerSettings() { const settingsForm = document.createElement('form'); console.log(settingsForm); settingsForm.innerHTML = ` <div style="display: flex; flex-direction: column; width: 100%; min-width: 500px; margin: 0 auto;"> <!-- Scrollable content container --> <div class="scrollable-content" style="flex: 1; overflow-y: auto; max-height: 300px; padding-right: 10px;"> <!-- Fields will be inserted here dynamically --> </div> <!-- Buttons at the bottom --> <div class="settings-buttons" style="text-align: center; padding-top: 10px;"> <button type="submit" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 8px 16px; border-radius: 5px; margin-right: 10px; cursor: pointer;">Save Settings</button> <button type="button" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer;" id="cancel-button">Cancel</button> </div> </div> `; const fieldsContainer = settingsForm.querySelector('.scrollable-content'); // For each randomizable field, create settings UI const fields = ['emoji', 'name', 'sex', 'species', 'age', 'bodyType', 'personality', 'bio']; fields.forEach(field => { const fieldDiv = document.createElement('div'); fieldDiv.style.marginBottom = '10px'; const label = document.createElement('label'); label.style.color = 'var(--text-color)'; label.style.fontSize = '12px'; label.textContent = `Randomizer for ${capitalizeFirstLetter(field)}:`; // Dropdown for default options const select = document.createElement('select'); select.name = field; select.style.cssText = '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;'; // Populate options const options = defaultRandomizerOptions[field]; for (const optionName in options) { const option = document.createElement('option'); option.value = optionName; option.textContent = optionName; if (randomizerSettings[field].selectedOption === optionName && !randomizerSettings[field].customList.trim()) { option.selected = true; } select.appendChild(option); } // Custom list textarea const textarea = document.createElement('textarea'); textarea.name = field + '_custom'; textarea.rows = 2; textarea.placeholder = 'Enter custom list, separated by commas or new lines'; textarea.style.cssText = '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; margin-top: 5px;'; textarea.value = randomizerSettings[field].customList; // If custom list is non-empty, disable the select if (randomizerSettings[field].customList.trim() !== '') { select.disabled = true; } // Event listener to disable select if textarea has content textarea.addEventListener('input', (e) => { if (textarea.value.trim() !== '') { select.disabled = true; } else { select.disabled = false; } }); fieldDiv.appendChild(label); fieldDiv.appendChild(select); fieldDiv.appendChild(textarea); fieldsContainer.appendChild(fieldDiv); }); const cancelButton = settingsForm.querySelector('#cancel-button'); cancelButton.addEventListener('click', () => { closeModal(settingsModal); }); const settingsModal = createModal('Randomizer Settings', settingsForm); settingsForm.addEventListener('submit', (e) => { e.preventDefault(); // Update randomizerSettings fields.forEach(field => { const select = settingsForm[field]; const textarea = settingsForm[field + '_custom']; randomizerSettings[field].customList = textarea.value.trim(); if (randomizerSettings[field].customList !== '') { randomizerSettings[field].selectedOption = ''; } else { randomizerSettings[field].selectedOption = select.value; } }); closeModal(settingsModal); }); settingsForm.parentNode.style.backgroundColor = 'var(--bg-color-full)'; } // Function to edit a character function editCharacter(character) { const lockState = { emoji: false, name: false, sex: false, species: false, age: false, bodyType: false, personality: false, bio: false }; const editForm = document.createElement('form'); editForm.innerHTML = ` <div style="width: 100%; min-width: 500px; margin: 0 auto; position: relative;"> ${createField('Emoji', 'emoji', character.emoji)} ${createField('Name', 'name', character.name)} ${createSexDropdown('Sex', 'sex', character.sex)} ${createField('Species', 'species', character.species)} ${createField('Age', 'age', character.age)} ${createField('Body Type', 'bodyType', character.bodyType)} ${createField('Personality', 'personality', character.personality)} ${createTextareaField('Bio', 'bio', character.bio)} <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> `; // Helper functions to create fields with lock buttons function createField(labelText, fieldName, value) { return ` <label style="color: var(--text-color); font-size: 12px;"> ${labelText}: <div style="position: relative;"> <input type="text" name="${fieldName}" value="${value}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: calc(100% - 35px); box-sizing: border-box;"/> <button type="button" class="lock-button" data-field="${fieldName}" style="position: absolute; right: 0; top: 0; height: 100%; width: 35px; background-color: var(--button-bg-color); border: 1px solid var(--border-color); border-radius: 5px; cursor: pointer; color: var(--text-color);">${lockState[fieldName] ? '🔒' : '🔓'}</button> </div> </label><br> `; } function createSexDropdown(labelText, fieldName, value) { return ` <label style="color: var(--text-color); font-size: 12px;"> ${labelText}: <div style="position: relative;"> <select name="${fieldName}" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: calc(100% - 35px); box-sizing: border-box;"> <option value="male" ${value === 'male' ? 'selected' : ''}>Male</option> <option value="female" ${value === 'female' ? 'selected' : ''}>Female</option> <option value="other" ${value === 'other' ? 'selected' : ''}>Other</option> </select> <button type="button" class="lock-button" data-field="${fieldName}" style="position: absolute; right: 0; top: 0; height: 100%; width: 35px; background-color: var(--button-bg-color); border: 1px solid var(--border-color); border-radius: 5px; cursor: pointer; color: var(--text-color);">${lockState[fieldName] ? '🔒' : '🔓'}</button> </div> </label><br> `; } function createTextareaField(labelText, fieldName, value) { return ` <label style="color: var(--text-color); font-size: 12px;"> ${labelText}: <div style="position: relative;"> <textarea name="${fieldName}" 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: calc(100% - 35px); box-sizing: border-box;">${value}</textarea> <button type="button" class="lock-button" data-field="${fieldName}" style="position: absolute; right: 0; top: 0; height: 100%; width: 35px; background-color: var(--button-bg-color); border: 1px solid var(--border-color); border-radius: 5px; cursor: pointer; color: var(--text-color);">${lockState[fieldName] ? '🔒' : '🔓'}</button> </div> </label><br> `; } // Copy Tooltip button const copyButton = document.createElement('button'); copyButton.textContent = '📋'; copyButton.title = 'Copy Tooltip'; copyButton.type = 'button'; // Prevent form submission 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); // Randomize button const randomizeButton = document.createElement('button'); randomizeButton.textContent = '🎲'; randomizeButton.title = 'Randomize'; randomizeButton.type = 'button'; // Prevent form submission randomizeButton.style.cssText = ` position: absolute; top: 15px; right: 90px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; randomizeButton.addEventListener('click', () => { const randomCharacter = generateRandomCharacter(); const fields = ['emoji', 'name', 'sex', 'species', 'age', 'bodyType', 'personality', 'bio']; fields.forEach((field) => { if (!lockState[field]) { editForm[field].value = randomCharacter[field]; } }); }); editForm.appendChild(randomizeButton); // Settings button const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙️'; settingsButton.title = 'Randomizer Settings'; settingsButton.type = 'button'; // Prevent form submission settingsButton.style.cssText = ` position: absolute; top: 15px; right: 130px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; settingsButton.addEventListener('click', () => { openRandomizerSettings(); }); editForm.appendChild(settingsButton); // Append the form to a modal or the desired container const modal = createModal('Edit Character', editForm); // Lock button functionality const lockButtons = editForm.querySelectorAll('.lock-button'); lockButtons.forEach((button) => { const fieldName = button.getAttribute('data-field'); button.addEventListener('click', () => { lockState[fieldName] = !lockState[fieldName]; // Update button icon based on state button.textContent = lockState[fieldName] ? '🔒' : '🔓'; }); }); 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(); } } // Default randomizer options for locations const defaultRandomizerOptionsAlt = { emoji: { 'Buildings': ['🏠', '🏡', '🏫', '🏭', '🏢', '🏰', '🏩'], 'Nature': ['🌲', '🌳', '🌴', '🌵', '🌾', '🌊', '🌋'], // Add more categories if needed }, name: { 'Common': ['Park', 'Museum', 'Library', 'Coffee Shop', 'Restaurant', 'Beach', 'Mountain', 'Lake', 'Forest', 'City'], 'Fantasy': ['Dragon’s Lair', 'Enchanted Forest', 'Mystic Mountain', 'Crystal Lake', 'Forbidden City'], // Add more categories if needed }, description: { 'Standard': [ 'A beautiful place to visit.', 'Known for its stunning views.', 'A popular spot among locals.', 'Rich in history and culture.', 'Famous for its delicious cuisine.', 'Home to many rare species.', 'A tranquil escape from the city.', 'A bustling hub of activity.', 'A hidden gem waiting to be explored.', 'An iconic landmark.' ], // Add more categories if needed }, }; // User's randomizer settings for locations const randomizerSettingsAlt = { emoji: { selectedOption: 'Buildings', customList: '' }, name: { selectedOption: 'Common', customList: '' }, description: { selectedOption: 'Standard', customList: '' }, }; // Function to generate a random location function generateRandomLocationAlt() { const location = {}; function getRandomItem(field) { let items = []; if (randomizerSettingsAlt[field].customList.trim() !== '') { // Use custom list items = randomizerSettingsAlt[field].customList.split(/[,\n]+/).map(item => item.trim()).filter(item => item !== ''); } else { // Use selected default option const selectedOption = randomizerSettingsAlt[field].selectedOption; items = defaultRandomizerOptionsAlt[field][selectedOption]; } // Handle cases where items might be undefined or empty if (!items || items.length === 0) { return ''; } // Randomly pick an item return items[Math.floor(Math.random() * items.length)]; } location.emoji = getRandomItem('emoji'); location.name = getRandomItem('name'); location.description = getRandomItem('description'); return location; } // Function to open Randomizer Settings dialog for locations function openRandomizerSettingsAlt() { const settingsForm = document.createElement('form'); settingsForm.innerHTML = ` <div style="display: flex; flex-direction: column; width: 100%; min-width: 500px; margin: 0 auto;"> <!-- Scrollable content container --> <div class="scrollable-content" style="flex: 1; overflow-y: auto; max-height: 300px; padding-right: 10px;"> <!-- Fields will be inserted here dynamically --> </div> <!-- Buttons at the bottom --> <div class="settings-buttons" style="text-align: center; padding-top: 10px;"> <button type="submit" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 8px 16px; border-radius: 5px; margin-right: 10px; cursor: pointer;">Save Settings</button> <button type="button" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer;" id="cancel-button-alt">Cancel</button> </div> </div> `; const fieldsContainer = settingsForm.querySelector('.scrollable-content'); // For each randomizable field, create settings UI const fields = ['emoji', 'name', 'description']; fields.forEach(field => { const fieldDiv = document.createElement('div'); fieldDiv.style.marginBottom = '10px'; const label = document.createElement('label'); label.style.color = 'var(--text-color)'; label.style.fontSize = '12px'; label.textContent = `Randomizer for ${capitalizeFirstLetter(field)}:`; // Dropdown for default options const select = document.createElement('select'); select.name = field; select.style.cssText = '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;'; // Populate options const options = defaultRandomizerOptionsAlt[field]; for (const optionName in options) { const option = document.createElement('option'); option.value = optionName; option.textContent = optionName; if (randomizerSettingsAlt[field].selectedOption === optionName && !randomizerSettingsAlt[field].customList.trim()) { option.selected = true; } select.appendChild(option); } // Custom list textarea const textarea = document.createElement('textarea'); textarea.name = field + '_custom'; textarea.rows = 2; textarea.placeholder = 'Enter custom list, separated by commas or new lines'; textarea.style.cssText = '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; margin-top: 5px;'; textarea.value = randomizerSettingsAlt[field].customList; // If custom list is non-empty, disable the select if (randomizerSettingsAlt[field].customList.trim() !== '') { select.disabled = true; } // Event listener to disable select if textarea has content textarea.addEventListener('input', (e) => { if (textarea.value.trim() !== '') { select.disabled = true; } else { select.disabled = false; } }); fieldDiv.appendChild(label); fieldDiv.appendChild(select); fieldDiv.appendChild(textarea); fieldsContainer.appendChild(fieldDiv); }); const cancelButton = settingsForm.querySelector('#cancel-button-alt'); cancelButton.addEventListener('click', () => { closeModal(settingsModal); }); const settingsModal = createModal('Randomizer Settings', settingsForm); settingsForm.addEventListener('submit', (e) => { e.preventDefault(); // Update randomizerSettingsAlt fields.forEach(field => { const select = settingsForm[field]; const textarea = settingsForm[field + '_custom']; randomizerSettingsAlt[field].customList = textarea.value.trim(); if (randomizerSettingsAlt[field].customList !== '') { randomizerSettingsAlt[field].selectedOption = ''; } else { randomizerSettingsAlt[field].selectedOption = select.value; } }); closeModal(settingsModal); }); settingsForm.parentNode.style.backgroundColor = 'var(--bg-color-full)'; } // Function to edit a location with randomize feature function editLocation(location) { const lockState = { emoji: false, name: false, description: false }; const editForm = document.createElement('form'); editForm.innerHTML = ` <div style="width: 100%; min-width: 500px; margin: 0 auto; position: relative;"> ${createFieldAlt('Emoji', 'emoji', location.emoji)} ${createFieldAlt('Name', 'name', location.name)} ${createTextareaFieldAlt('Description', 'description', location.description)} <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> `; // Helper functions to create fields with lock buttons function createFieldAlt(labelText, fieldName, value) { return ` <label style="color: var(--text-color); font-size: 12px;"> ${labelText}: <div style="position: relative;"> <input type="text" name="${fieldName}" value="${value}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: calc(100% - 35px); box-sizing: border-box;"/> <button type="button" class="lock-button-alt" data-field="${fieldName}" style="position: absolute; right: 0; top: 0; height: 100%; width: 35px; background-color: var(--button-bg-color); border: 1px solid var(--border-color); border-radius: 5px; cursor: pointer; color: var(--text-color);">${lockState[fieldName] ? '🔒' : '🔓'}</button> </div> </label><br> `; } function createTextareaFieldAlt(labelText, fieldName, value) { return ` <label style="color: var(--text-color); font-size: 12px;"> ${labelText}: <div style="position: relative;"> <textarea name="${fieldName}" 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: calc(100% - 35px); box-sizing: border-box;">${value}</textarea> <button type="button" class="lock-button-alt" data-field="${fieldName}" style="position: absolute; right: 0; top: 0; height: 100%; width: 35px; background-color: var(--button-bg-color); border: 1px solid var(--border-color); border-radius: 5px; cursor: pointer; color: var(--text-color);">${lockState[fieldName] ? '🔒' : '🔓'}</button> </div> </label><br> `; } // Copy Tooltip button const copyButton = document.createElement('button'); copyButton.textContent = '📋'; copyButton.title = 'Copy Tooltip'; copyButton.type = 'button'; // Prevent form submission 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); // Randomize button const randomizeButton = document.createElement('button'); randomizeButton.textContent = '🎲'; randomizeButton.title = 'Randomize'; randomizeButton.type = 'button'; // Prevent form submission randomizeButton.style.cssText = ` position: absolute; top: 15px; right: 90px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; randomizeButton.addEventListener('click', () => { const randomLocation = generateRandomLocationAlt(); const fields = ['emoji', 'name', 'description']; fields.forEach((field) => { if (!lockState[field]) { editForm[field].value = randomLocation[field]; } }); }); editForm.appendChild(randomizeButton); // Settings button const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙️'; settingsButton.title = 'Randomizer Settings'; settingsButton.type = 'button'; // Prevent form submission settingsButton.style.cssText = ` position: absolute; top: 15px; right: 130px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-color); `; settingsButton.addEventListener('click', () => { openRandomizerSettingsAlt(); }); editForm.appendChild(settingsButton); const modal = createModal('Edit Location', editForm); // Lock button functionality const lockButtons = editForm.querySelectorAll('.lock-button-alt'); lockButtons.forEach((button) => { const fieldName = button.getAttribute('data-field'); button.addEventListener('click', () => { lockState[fieldName] = !lockState[fieldName]; // Update button icon based on state button.textContent = lockState[fieldName] ? '🔒' : '🔓'; }); }); 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 }; // Helper function to find an item by ID function findItemById(groups, id) { let result = null; for (let group of groups) { if (group.id === id) { return group; } if (group.items) { result = findItemInItems(group.items, id); if (result) { return result; } } } return null; } function findItemInItems(items, id) { for (let item of items) { if (item.id === id) { return item; } else if (item.children && item.children.length > 0) { let found = findItemInItems(item.children, id); if (found) return found; } } return null; } function findGroupIndexByName(groups, name) { for (let i = 0; i < groups.length; i++) { if (groups[i].name === name) { return i; } } return null; } // Helper function to remove an item by ID function removeItemById(groups, id) { for (let group of groups) { if (group.items) { group.items = removeItemFromItems(group.items, id); } } } function removeItemFromItems(items, id) { return items.filter(item => { if (item.id === id) { return false; } else if (item.children && item.children.length > 0) { item.children = removeItemFromItems(item.children, id); } return true; }); } // Helper function to check for circular references function isAncestor(itemId, possibleAncestorId, type) { let targetItem = null; if (type === 'character') { targetItem = findItemById(characterGroups, itemId); } else { targetItem = findItemById(locationGroups, itemId); } if (!targetItem) return false; function searchChildren(item) { if (item.id === possibleAncestorId) { return true; } if (item.children && item.children.length > 0) { for (let child of item.children) { if (searchChildren(child)) { return true; } } } if (item.items && item.items.length > 0) { for (let child of item.items) { if (searchChildren(child)) { return true; } } } return false; } return searchChildren(targetItem); } // Function to create a radial tree visualization function createRadialTree(data, type) { // Variables to store settings and use them globally c_radius = type === 'character' ? (window.c_radius || 300) : 300; c_labelFontSize = type === 'character' ? (window.c_labelFontSize || 12) : 12; c_nodeSize = type === 'character' ? (window.c_nodeSize || 24) : 24; l_radius = type === 'location' ? (window.l_radius || 300) : 300; l_labelFontSize = type === 'location' ? (window.l_labelFontSize || 12) : 12; l_nodeSize = type === 'location' ? (window.l_nodeSize || 24) : 24; ItemTransferTarget = null; // 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'; // New Refresh Button const NItemButton = document.createElement('button'); NItemButton.textContent = '📝'; NItemButton.title = 'New Item'; NItemButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; buttonContainer.appendChild(NItemButton); NItemButton.addEventListener('click', () => { if(type === 'character'){createNewCharacter();}else{createNewLocation();} ItemTransferTarget = null; refreshTree(svgContainer, data, type); }); // New Refresh Button const NGroupButton = document.createElement('button'); NGroupButton.textContent = '🗄️'; NGroupButton.title = 'New Group'; NGroupButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; buttonContainer.appendChild(NGroupButton); NGroupButton.addEventListener('click', () => { if(type === 'character'){createNewCharacterGroup();}else{createNewLocationGroup();} ItemTransferTarget = null; refreshTree(svgContainer, data, type); }); // New Refresh Button const refreshButton = document.createElement('button'); refreshButton.textContent = '🔄'; refreshButton.title = 'Refresh Tree'; refreshButton.style.cssText = ` background: none; border: none; color: var(--text-color); cursor: pointer; margin-right: 5px; `; buttonContainer.appendChild(refreshButton); // Settings Button 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); // Close Button 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; ItemTransferTarget = null; }); 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'; ItemTransferTarget = null; refreshTree(svgContainer, data, type); }); 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') { window.c_radius = parseInt(nodeDistanceSlider.value, 10); window.c_labelFontSize = parseInt(labelSizeSlider.value, 10); window.c_nodeSize = parseInt(nodeSizeSlider.value, 10); } else { window.l_radius = parseInt(nodeDistanceSlider.value, 10); window.l_labelFontSize = parseInt(labelSizeSlider.value, 10); window.l_nodeSize = parseInt(nodeSizeSlider.value, 10); } settingsPanel.style.display = 'none'; // Regenerate the tree with new settings generateRadialTreeVisualization(svgContainer, data, type, type === 'character' ? window.c_radius : window.l_radius, type === 'character' ? window.c_labelFontSize : window.l_labelFontSize, type === 'character' ? window.c_nodeSize : window.l_nodeSize); }); // Add event listener to refresh button refreshButton.addEventListener('click', () => { ItemTransferTarget = null; refreshTree(svgContainer, data, type); }); // 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 refreshTree(container, data, type) { // Clear the existing SVG container (to remove the old tree) container.innerHTML = ''; // Re-use the same data, or if necessary, fetch new data here const structuredData = structureData(type === 'character' ? characterGroups : locationGroups); // Store the current zoom, pan, and rotation before refresh const currentTransform = window.currentTransform || { x: container.clientWidth / 2, y: container.clientHeight / 2, k: 1 }; const currentRotation = window.currentRotation || 0; // Regenerate the tree visualization with the existing settings const radius = type === 'character' ? window.c_radius : window.l_radius; const labelFontSize = type === 'character' ? window.c_labelFontSize : window.l_labelFontSize; const nodeSize = type === 'character' ? window.c_nodeSize : window.l_nodeSize; // Regenerate the tree and apply the stored transformation generateRadialTreeVisualization(container, structuredData, type, radius, labelFontSize, nodeSize, currentTransform, currentRotation); } function generateRadialTreeVisualization(container, data, type, radius = 300, labelFontSize = 12, nodeSize = 24, initialTransform = null, initialRotation = 0) { // Clear the container container.innerHTML = ''; // Set dimensions const width = container.clientWidth; const height = container.clientHeight; // Current zoom, pan, and rotation state let currentRotation = initialRotation; let currentTransform = initialTransform || { 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})`); // Apply the initial zoom, pan, and rotation d3.select(container).select('svg').call(zoomBehavior.transform, d3.zoomIdentity.translate(currentTransform.x, currentTransform.y).scale(currentTransform.k)); 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})`); // Store the transformation globally so we can retain it after refresh window.currentTransform = currentTransform; } // 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(--muted-bg-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', d => (ItemTransferTarget && d.data.id === ItemTransferTarget.id) ? 'var(--warning-bg-color)' : '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(); if (ItemTransferTarget == null) { // Check if it's an item (not a group) if (d.data.description != undefined) { // items undefined means it's not a group CreateContextMenu( 'var(--button-bg-color)', 'var(--text-color)', ['Locate', 'Edit', 'Move', 'Delete'], [ // Locate function() { navigateToItem(d.data.id, type); }, // Edit function() { const itemId = d.data.id; if (type === 'character') { const character = findItemById(characterGroups, itemId); if (character) { editCharacter(character); renderCharacterGroups(); refreshTree(container, data, type); } } else { const location = findItemById(locationGroups, itemId); if (location) { editLocation(location); renderLocationGroups(); refreshTree(container, data, type); } } }, // Move function() { ItemTransferTarget = d.data; refreshTree(container, data, type); }, // Delete function() { const itemId = d.data.id; if (type === 'character') { deleteCharacter(itemId); renderCharacterGroups(); refreshTree(container, data, type); } else { deleteLocation(itemId); renderLocationGroups(); refreshTree(container, data, type); } } ] ); } else { // It's a group CreateContextMenu( 'var(--button-bg-color)', 'var(--text-color)', ['Edit', 'Delete'], [ // Edit Group function() { const groupName = d.data.name; if (type === 'character') { const groupIndex = findGroupIndexByName(characterGroups, groupName); if (groupIndex !== null) { renameGroup(groupIndex); renderCharacterGroups(); refreshTree(container, data, type); } } else { const groupIndex = findGroupIndexByName(locationGroups, groupName); if (groupIndex !== null) { renameLocationGroup(groupIndex); renderLocationGroups(); refreshTree(container, data, type); } } }, // Delete Group function() { const groupName = d.data.name; if (type === 'character') { const groupIndex = findGroupIndexByName(characterGroups, groupName); if (groupIndex !== null) { deleteGroup(groupIndex); renderCharacterGroups(); refreshTree(container, data, type); } } else { const groupIndex = findGroupIndexByName(locationGroups, groupName); if (groupIndex !== null) { deleteLocationGroup(groupIndex); renderLocationGroups(); refreshTree(container, data, type); } } } ] ); } } else { // Moving an item if (ItemTransferTarget.id === d.data.id) { // Stop moving CreateContextMenu( 'var(--button-bg-color)', 'var(--text-color)', ['Stop'], [ function() { ItemTransferTarget = null; refreshTree(container, data, type); } ] ); } else { // Place the item CreateContextMenu( 'var(--button-bg-color)', 'var(--text-color)', ['Place'], [ function() { // Check for circular reference if (isAncestor(ItemTransferTarget.id, d.data.id, type)) { alert('Cannot move an item into its descendant.'); return; } if (d.data.description != undefined) { if (type === 'character') { moveCharacterToParent(ItemTransferTarget.id, findItemById(characterGroups, d.data.id)); } else { moveLocationToParent(ItemTransferTarget.id, findItemById(locationGroups, d.data.id)); } } else { // New parent is a group const groupName = d.data.name; if (type === 'character') { const groupIndex = findGroupIndexByName(locationGroups, groupName); moveCharacterToGroup(ItemTransferTarget.id, groupIndex); } else { const groupIndex = findGroupIndexByName(locationGroups, groupName); moveLocationToGroup(ItemTransferTarget.id, groupIndex); } } // Clear transfer target ItemTransferTarget = null; // Refresh if (type === 'character') { renderCharacterGroups(); } else { renderLocationGroups(); } refreshTree(container, data, type); } ] ); } } showMenu(event); }); // --- 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(${currentRotation}deg); /* Apply initial rotation */ `; // 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)`; // Store the updated rotation globally so we can retain it after refresh window.currentRotation = currentRotation; // Update previous angle for the next move previousAngle = currentAngle; } }); // Stop dragging when mouse is released document.addEventListener('mouseup', () => { isDragging = false; }); // 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>`; } })();