// ==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>`;
}
})();