// ==UserScript==
// @name JanitorAI Context Maker
// @namespace http://tampermonkey.net/
// @version 3.6
// @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
// ==/UserScript==
(async function() {
'use strict';
// Define Themes
const themes = {
dark: {
'--bg-color': 'rgba(34, 34, 34, var(--ui-transparency))',
'--bg-tool': 'rgba(34, 34, 34, 0.8)',
'--text-color': '#ffffff',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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',
'--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`;
infoText.style.cssText = `
color: var(--text-color);
`;
collapsedInfo.appendChild(infoText);
groupContainer.appendChild(collapsedInfo);
}
// Add dragover and drop events for grouping
groupContainer.addEventListener('dragover', (e) => {
e.preventDefault();
groupContainer.style.border = `2px dashed var(--info-bg-color)`;
});
groupContainer.addEventListener('dragleave', (e) => {
groupContainer.style.border = `1px solid var(--border-color)`;
});
groupContainer.addEventListener('drop', (e) => {
e.preventDefault();
groupContainer.style.border = `1px solid var(--border-color)`;
const data = e.dataTransfer.getData('text/plain');
const { type, id } = JSON.parse(data);
if (type === 'character') {
moveCharacterToGroup(id, groupIndex);
}
});
characterGroupList.appendChild(groupContainer);
});
// Ensure at least one group exists
if (characterGroups.length === 0) {
characterGroups.push({ name: 'Ungrouped', items: [], collapsed: false });
renderCharacterGroups();
}
}
// Function to update the Toggle All Button appearance
function updateToggleAllButton(button, group) {
const activeCount = countActiveCharacters(group.items);
const totalCount = countTotalCharacters(group.items);
if (activeCount === totalCount && totalCount > 0) {
// All active
button.textContent = '🟢';
} else if (activeCount === 0) {
// All inactive
button.textContent = '🔴';
} else {
// Mixed
button.textContent = '🟡';
}
}
function countActiveCharacters(items) {
let count = 0;
items.forEach(item => {
if (item.active) count++;
if (item.children && item.children.length > 0) {
count += countActiveCharacters(item.children);
}
});
return count;
}
function countTotalCharacters(items) {
let count = items.length;
items.forEach(item => {
if (item.children && item.children.length > 0) {
count += countTotalCharacters(item.children);
}
});
return count;
}
// Function to toggle all characters in a group
function toggleAllGroupCharacters(group) {
const allActive = countActiveCharacters(group.items) === countTotalCharacters(group.items);
toggleCharacters(group.items, !allActive);
}
function toggleCharacters(items, state) {
items.forEach(item => {
item.active = state;
if (item.children && item.children.length > 0) {
toggleCharacters(item.children, state);
}
});
}
// Function to generate tooltip content for a character
function getCharacterTooltipContent(character) {
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`;
infoText.style.cssText = `
color: var(--text-color);
`;
collapsedInfo.appendChild(infoText);
container.appendChild(collapsedInfo);
}
}
return container;
}
function updateAllToggleButtons() {
characterGroups.forEach((group, groupIndex) => {
const toggleAllButton = toggleAllButtonForGroup(groupIndex);
if (toggleAllButton) {
updateToggleAllButton(toggleAllButton, group);
}
});
}
// Function to get the toggle all button for a group
function toggleAllButtonForGroup(groupIndex) {
const groupContainer = characterGroupList.children[groupIndex];
if (groupContainer) {
const toggleAllButton = groupContainer.querySelector('button[title="Activate/Deactivate All"]');
return toggleAllButton;
}
return null;
}
// Function to rename a group
function renameGroup(groupIndex) {
const newName = prompt('Enter new group name:', characterGroups[groupIndex].name);
if (newName && newName.trim() !== '') {
characterGroups[groupIndex].name = newName.trim();
renderCharacterGroups();
}
}
// Function to delete a group
function deleteGroup(groupIndex) {
if (characterGroups.length === 1) {
alert('At least one group must exist.');
return;
}
if (confirm(`Are you sure you want to delete the group "${characterGroups[groupIndex].name}"? All characters in this group will be moved to "Ungrouped".`)) {
const group = characterGroups.splice(groupIndex, 1)[0];
const ungrouped = characterGroups.find(g => g.name === 'Ungrouped');
if (ungrouped) {
ungrouped.items = ungrouped.items.concat(group.items);
} else {
characterGroups.unshift({ name: 'Ungrouped', items: group.items, collapsed: false });
}
renderCharacterGroups();
}
}
// Function to edit a character
function editCharacter(character) {
const editForm = document.createElement('form');
editForm.innerHTML = `
<div style="width: 100%; min-width: 500px; margin: 0 auto;">
<label style="color: var(--text-color); font-size: 12px;">Emoji: <input type="text" name="emoji" value="${character.emoji}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Name: <input type="text" name="name" value="${character.name}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Sex: <input type="text" name="sex" value="${character.sex}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Species: <input type="text" name="species" value="${character.species}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Age: <input type="text" name="age" value="${character.age}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Body Type: <input type="text" name="bodyType" value="${character.bodyType}" autocomplete="off" style="background-color: var(--bg-color); color: var (--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Personality: <input type="text" name="personality" value="${character.personality}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Bio: <textarea name="bio" autocomplete="off" rows="4" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;">${character.bio}</textarea></label><br>
<label style="color: var(--text-color); font-size: 12px;">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;
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `all_characters_with_groups.json`;
link.click();
}
// Function to import characters with groups
function importCharacters() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedGroups = JSON.parse(e.target.result);
if (Array.isArray(importedGroups)) {
characterGroups = importedGroups;
renderCharacterGroups();
} else {
alert('Invalid format for characters import.');
}
} catch (error) {
alert('Failed to import characters: ' + error.message);
}
};
reader.readAsText(file);
});
fileInput.click();
}
// Function to export a single character
function exportCharacter(character) {
const json = JSON.stringify(character, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${character.name}.json`;
link.click();
}
// Function to move character to a group (uncoupling from parent)
function moveCharacterToGroup(characterId, targetGroupIndex) {
let character = null;
// Remove character from current group or parent
characterGroups.forEach(group => {
group.items = removeCharacterRecursive(group.items, characterId, (item) => {
character = item;
});
});
// Add to target group
if (character) {
characterGroups[targetGroupIndex].items.push(character);
renderCharacterGroups();
}
}
function removeCharacterRecursive(items, characterId, callback) {
return items.filter(item => {
if (item.id === characterId) {
callback(item);
return false;
}
if (item.children && item.children.length > 0) {
item.children = removeCharacterRecursive(item.children, characterId, callback);
}
return true;
});
}
// Function to move character under another character (nesting)
function moveCharacterToParent(characterId, parentCharacter) {
let character = null;
// Remove character from current group or parent
characterGroups.forEach(group => {
group.items = removeCharacterRecursive(group.items, characterId, (item) => {
character = item;
});
});
if (character) {
parentCharacter.children.push(character);
renderCharacterGroups();
}
}
// Utility function to generate unique IDs
function generateId() {
return '_' + Math.random().toString(36).substr(2, 9);
}
// --- Location Sidebar with Groups and Nested Locations ---
const locationSidebar = document.createElement('div');
locationSidebar.id = 'location-sheet-sidebar';
locationSidebar.style.cssText = `
position: fixed;
top: 0;
left: -350px;
width: 350px;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--bg-color);
border-right: 2px solid var(--border-color);
box-shadow: 2px 0 5px var(--shadow-color);
box-sizing: border-box;
transition: left 0.3s;
z-index: 9999;
`;
document.body.appendChild(locationSidebar);
const locationToggleButton = document.createElement('button');
locationToggleButton.textContent = '☰';
locationToggleButton.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
padding: 5px 10px;
border: none;
background-color: var(--button-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
transition: left 0.3s;
z-index: 10000;
`;
locationToggleButton.addEventListener('click', () => {
const isOpen = locationSidebar.style.left === '0px';
locationSidebar.style.left = isOpen ? '-350px' : '0';
locationToggleButton.style.left = isOpen ? '10px' : '360px';
});
document.body.appendChild(locationToggleButton);
const locationHeader = document.createElement('div');
locationHeader.style.cssText = `
padding: 10px;
background-color: var(--border-color);
text-align: center;
font-weight: bold;
color: var(--text-color);
`;
locationHeader.textContent = 'Locations';
locationSidebar.appendChild(locationHeader);
const locationButtonBox = document.createElement('div');
locationButtonBox.style.cssText = `
display: flex;
justify-content: space-between;
padding: 10px;
background-color: var(--bg-color);
flex-shrink: 0;
`;
locationSidebar.appendChild(locationButtonBox);
// New Location Button
const newLocationButton = document.createElement('button');
newLocationButton.textContent = 'New';
newLocationButton.title = 'Create New Location';
newLocationButton.style.cssText = `
padding: 5px 10px;
border: none;
background-color: var(--success-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
`;
newLocationButton.addEventListener('click', () => {
createNewLocation();
});
locationButtonBox.appendChild(newLocationButton);
// Load Location Button
const loadLocationButton = document.createElement('button');
loadLocationButton.textContent = 'Load';
loadLocationButton.title = 'Load a Location';
loadLocationButton.style.cssText = `
padding: 5px 10px;
border: none;
background-color: var(--info-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
`;
loadLocationButton.addEventListener('click', () => {
loadLocation();
});
locationButtonBox.appendChild(loadLocationButton);
// Group Location Button
const groupLocationButton = document.createElement('button');
groupLocationButton.textContent = 'Group';
groupLocationButton.title = 'Create New Group';
groupLocationButton.style.cssText = `
padding: 5px 10px;
border: none;
background-color: var(--info-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
`;
groupLocationButton.addEventListener('click', () => {
createNewLocationGroup();
});
locationButtonBox.appendChild(groupLocationButton);
// Import Location Button
const importLocationButton = document.createElement('button');
importLocationButton.textContent = 'Import';
importLocationButton.title = 'Import Locations';
importLocationButton.style.cssText = `
padding: 5px 10px;
border: none;
background-color: var(--warning-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
`;
importLocationButton.addEventListener('click', () => {
importLocations();
});
locationButtonBox.appendChild(importLocationButton);
// Export Location Button
const exportLocationButton = document.createElement('button');
exportLocationButton.textContent = 'Export';
exportLocationButton.title = 'Export Locations';
exportLocationButton.style.cssText = `
padding: 5px 10px;
border: none;
background-color: var(--muted-bg-color);
color: var(--text-color);
border-radius: 5px;
cursor: pointer;
`;
exportLocationButton.addEventListener('click', () => {
exportLocations();
});
locationButtonBox.appendChild(exportLocationButton);
// Add fifth button: "Load" for Location
function loadLocation() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const location = JSON.parse(e.target.result);
if (location && location.id) {
// Add to Ungrouped
const ungrouped = locationGroups.find(g => g.name === 'Ungrouped');
if (ungrouped) {
ungrouped.items.push(location);
} else {
locationGroups[0].items.push(location);
}
renderLocationGroups();
ensureActiveLocation();
} else {
alert('Invalid location format.');
}
} catch (error) {
alert('Failed to load location: ' + error.message);
}
};
reader.readAsText(file);
});
fileInput.click();
}
const locationGroupList = document.createElement('div');
locationGroupList.id = 'location-group-list';
locationGroupList.style.cssText = `
flex-grow: 1;
overflow-y: auto;
padding: 10px;
`;
locationSidebar.appendChild(locationGroupList);
// Initialize Location Groups with a single default location
let locationGroups = [
{
name: 'Ungrouped',
items: [
{
id: generateId(),
active: true,
emoji: '📍',
name: 'Default Location',
description: 'Description of the default location.',
children: [],
collapsed: false
}
],
collapsed: false
}
];
// Function to create a new Location
function createNewLocation() {
const newLocation = {
id: generateId(),
active: false,
emoji: '📍',
name: 'New Location',
description: '',
children: [],
collapsed: false
};
// Add to Ungrouped
const ungrouped = locationGroups.find(g => g.name === 'Ungrouped');
if (ungrouped) {
ungrouped.items.push(newLocation);
} else {
locationGroups[0].items.push(newLocation);
}
renderLocationGroups();
ensureActiveLocation();
}
// Function to create a new Location Group
function createNewLocationGroup() {
const groupName = prompt('Enter group name:', `Group ${locationGroups.length}`);
if (groupName && groupName.trim() !== '') {
locationGroups.push({ name: groupName.trim(), items: [], collapsed: false });
renderLocationGroups();
}
}
// Function to render Location Groups and Items
function renderLocationGroups() {
locationGroupList.innerHTML = '';
locationGroups.forEach((group, groupIndex) => {
// Sort items alphabetically by name
group.items.sort((a, b) => a.name.localeCompare(b.name));
const groupContainer = document.createElement('div');
groupContainer.className = 'location-group';
groupContainer.style.cssText = `
margin-bottom: 10px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--active-char-color);
`;
const groupHeader = document.createElement('div');
groupHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background-color: var(--button-bg-color);
color: var(--text-color);
cursor: pointer;
user-select: none;
position: relative;
`;
groupHeader.textContent = group.name;
groupHeader.addEventListener('click', () => {
group.collapsed = !group.collapsed;
renderLocationGroups();
});
const groupActions = document.createElement('div');
groupActions.style.cssText = `
display: flex;
align-items: center;
`;
// Rename Group Button
const renameGroupButton = document.createElement('button');
renameGroupButton.textContent = '✏️';
renameGroupButton.title = 'Rename Group';
renameGroupButton.style.cssText = `
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
margin-right: 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`;
infoText.style.cssText = `
color: var(--text-color);
`;
collapsedInfo.appendChild(infoText);
groupContainer.appendChild(collapsedInfo);
}
// Add dragover and drop events for grouping
groupContainer.addEventListener('dragover', (e) => {
e.preventDefault();
groupContainer.style.border = `2px dashed var(--info-bg-color)`;
});
groupContainer.addEventListener('dragleave', (e) => {
groupContainer.style.border = `1px solid var(--border-color)`;
});
groupContainer.addEventListener('drop', (e) => {
e.preventDefault();
groupContainer.style.border = `1px solid var(--border-color)`;
const data = e.dataTransfer.getData('text/plain');
const { type, id } = JSON.parse(data);
if (type === 'location') {
moveLocationToGroup(id, groupIndex);
}
});
locationGroupList.appendChild(groupContainer);
});
// Ensure at least one group exists
if (locationGroups.length === 0) {
locationGroups.push({ name: 'Ungrouped', items: [], collapsed: false });
renderLocationGroups();
}
// Ensure at least one location is active
ensureActiveLocation();
}
// Function to generate tooltip content for a location
function getLocationTooltipContent(location) {
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`;
infoText.style.cssText = `
color: var(--text-color);
`;
collapsedInfo.appendChild(infoText);
container.appendChild(collapsedInfo);
}
}
return container;
}
function deactivateLocationRecursive(location) {
location.active = false;
if (location.children && location.children.length > 0) {
location.children.forEach(child => {
deactivateLocationRecursive(child);
});
}
}
// Function to rename a location group
function renameLocationGroup(groupIndex) {
const newName = prompt('Enter new group name:', locationGroups[groupIndex].name);
if (newName && newName.trim() !== '') {
locationGroups[groupIndex].name = newName.trim();
renderLocationGroups();
}
}
// Function to delete a location group
function deleteLocationGroup(groupIndex) {
if (locationGroups.length === 1) {
alert('At least one group must exist.');
return;
}
if (confirm(`Are you sure you want to delete the group "${locationGroups[groupIndex].name}"? All locations in this group will be moved to "Ungrouped".`)) {
const group = locationGroups.splice(groupIndex, 1)[0];
const ungrouped = locationGroups.find(g => g.name === 'Ungrouped');
if (ungrouped) {
ungrouped.items = ungrouped.items.concat(group.items);
} else {
locationGroups.unshift({ name: 'Ungrouped', items: group.items, collapsed: false });
}
renderLocationGroups();
}
}
// Function to edit a location
function editLocation(location) {
const editForm = document.createElement('form');
editForm.innerHTML = `
<div style="width: 100%; min-width: 500px; margin: 0 auto;">
<label style="color: var(--text-color); font-size: 12px;">Emoji: <input type="text" name="emoji" value="${location.emoji}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Name: <input type="text" name="name" value="${location.name}" autocomplete="off" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;"></label><br>
<label style="color: var(--text-color); font-size: 12px;">Description: <textarea name="description" autocomplete="off" rows="4" style="background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 5px; padding: 5px; width: 100%; box-sizing: border-box;">${location.description}</textarea></label><br>
<button type="submit" style="background-color: var(--button-bg-color); color: var(--text-color); border: none; padding: 5px; border-radius: 5px; width: 100%; text-align: center; margin-top: 10px;">Save</button>
</div>
`;
// 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;
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `all_locations_with_groups.json`;
link.click();
}
// Function to import locations with groups
function importLocations() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedGroups = JSON.parse(e.target.result);
if (Array.isArray(importedGroups)) {
locationGroups = importedGroups;
renderLocationGroups();
ensureActiveLocation();
} else {
alert('Invalid format for locations import.');
}
} catch (error) {
alert('Failed to import locations: ' + error.message);
}
};
reader.readAsText(file);
});
fileInput.click();
}
// Function to export a single location
function exportLocation(location) {
const json = JSON.stringify(location, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${location.name}.json`;
link.click();
}
// Function to move location to a group (uncoupling from parent)
function moveLocationToGroup(locationId, targetGroupIndex) {
let location = null;
// Remove location from current group or parent
locationGroups.forEach(group => {
group.items = removeLocationRecursive(group.items, locationId, (item) => {
location = item;
});
});
// Add to target group
if (location) {
locationGroups[targetGroupIndex].items.push(location);
renderLocationGroups();
ensureActiveLocation();
}
}
function removeLocationRecursive(items, locationId, callback) {
return items.filter(item => {
if (item.id === locationId) {
callback(item);
return false;
}
if (item.children && item.children.length > 0) {
item.children = removeLocationRecursive(item.children, locationId, callback);
}
return true;
});
}
// Function to move location under another location (nesting)
function moveLocationToParent(locationId, parentLocation) {
let location = null;
// Remove location from current group or parent
locationGroups.forEach(group => {
group.items = removeLocationRecursive(group.items, locationId, (item) => {
location = item;
});
});
if (location) {
if(parentLocation.children == undefined){
parentLocation.children = [];
}
parentLocation.children.push(location);
renderLocationGroups();
ensureActiveLocation();
}
}
// Function to create a modal
function createModal(title, content) {
const modalOverlay = document.createElement('div');
modalOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10002;
`;
const modalBox = document.createElement('div');
modalBox.style.cssText = `
background-color: var(--bg-color);
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 5px;
width: fit-content;
min-width: 500px;
max-width: 90%;
box-shadow: 0 0 10px var(--shadow-color);
position: relative;
`;
const modalTitle = document.createElement('h2');
modalTitle.textContent = title;
modalTitle.style.cssText = `
margin-top: 0;
color: var(--text-color);
`;
modalBox.appendChild(modalTitle);
const closeButton = document.createElement('button');
closeButton.textContent = '✖️';
closeButton.style.cssText = `
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--text-color);
`;
closeButton.addEventListener('click', () => {
closeModal(modalOverlay);
});
modalBox.appendChild(closeButton);
modalBox.appendChild(content);
modalOverlay.appendChild(modalBox);
document.body.appendChild(modalOverlay);
return modalOverlay;
}
// Function to close a modal
function closeModal(modal) {
if (modal && modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Function to ensure at least one location is active
function ensureActiveLocation() {
const activeLocations = [];
locationGroups.forEach(group => {
group.items.forEach(loc => {
collectActiveLocations(loc, activeLocations);
});
});
if (activeLocations.length === 0 && locationGroups.flatMap(g => g.items).length > 0) {
locationGroups[0].items[0].active = true;
}
}
function collectActiveLocations(location, activeLocations) {
if (location.active) {
activeLocations.push(location);
}
if (location.children && location.children.length > 0) {
location.children.forEach(child => {
collectActiveLocations(child, activeLocations);
});
}
}
// Initial render of Character and Location Groups
renderCharacterGroups();
renderLocationGroups();
// --- Plot Field ---
const plotDiv = document.createElement('div');
plotDiv.style.cssText = `
display: flex;
justify-content: space-between;
padding: 10px;
background-color: var(--bg-color);
border-top: 1px solid var(--border-color);
flex-shrink: 0;
`;
const plotInput = document.createElement('input');
plotInput.type = 'text';
plotInput.placeholder = 'Plot';
plotInput.style.cssText = `
width: 100%;
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--bg-color);
color: var(--text-color);
box-sizing: border-box;
`;
plotDiv.appendChild(plotInput);
characterSidebar.appendChild(plotDiv);
// --- Location Context Inputs ---
const globalInputs = document.createElement('div');
globalInputs.style.cssText = `
display: flex;
justify-content: space-between;
padding: 10px;
background-color: var(--bg-color);
border-top: 1px solid var(--border-color);
flex-shrink: 0;
`;
locationSidebar.appendChild(globalInputs);
const timeInput = document.createElement('input');
timeInput.type = 'text';
timeInput.placeholder = 'Time';
timeInput.style.cssText = `
width: calc(50% - 5px);
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--bg-color);
color: var(--text-color);
`;
globalInputs.appendChild(timeInput);
const weatherInput = document.createElement('input');
weatherInput.type = 'text';
weatherInput.placeholder = 'Weather';
weatherInput.style.cssText = `
width: calc(50% - 5px);
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--bg-color);
color: var(--text-color);
`;
globalInputs.appendChild(weatherInput);
// Initial render of transparency and theme settings
transparencySlider.value = defaultTransparency;
themeDropdown.value = defaultTheme;
})();