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