Discord/Shapes Tools

Various Functions for Shapes.inc for Discord

  1. // ==UserScript==
  2. // @name Discord/Shapes Tools
  3. // @namespace https://vishanka.com
  4. // @version 3.3
  5. // @description Various Functions for Shapes.inc for Discord
  6. // @author Vishanka
  7. // @match https://discord.com/channels/*
  8. // @grant GM_addStyle
  9. // @grant GM_xmlhttpRequest
  10. // @license Proprietary
  11. // @run-at document-idle
  12. // ==/UserScript==
  13.  
  14.  
  15. (function() {
  16. 'use strict';
  17. let notifierActive = false;
  18.  
  19. // ================================================================== LOREBOOK ==================================================================
  20. function LorebookScript(){
  21.  
  22. // Create and add the arrow button to open the storage panel
  23. const arrowButton = document.createElement('div');
  24. arrowButton.innerHTML = '〈';
  25. arrowButton.style.position = 'fixed';
  26. arrowButton.style.bottom = '50%';
  27. arrowButton.style.right = '0';
  28. arrowButton.style.padding = '10px';
  29. arrowButton.style.fontSize = '24px';
  30. arrowButton.style.zIndex = '1000';
  31. arrowButton.style.cursor = 'pointer';
  32. arrowButton.style.color = '#B4B4B4';
  33. arrowButton.style.borderRadius = '5px 0 0 5px';
  34. arrowButton.style.transition = 'transform 0.3s ease, right 0.3s ease, background-color 0.1s';
  35.  
  36. // Toggle panel visibility
  37. arrowButton.addEventListener('click', () => {
  38. if (DCstoragePanel.style.right === '-250px') {
  39. DCstoragePanel.style.right = '0';
  40. arrowButton.style.right = '250px';
  41. arrowButton.style.transform = 'rotate(180deg)';
  42. } else {
  43. DCstoragePanel.style.right = '-250px';
  44. arrowButton.style.right = '0';
  45. arrowButton.style.transform = 'rotate(0deg)';
  46. }
  47. });
  48.  
  49. // Create the fancy sliding panel
  50. window.DCstoragePanel = document.createElement('div');
  51. DCstoragePanel.style.position = 'fixed';
  52. DCstoragePanel.style.top = '0';
  53. DCstoragePanel.style.right = '-250px'; // Initially hidden
  54. DCstoragePanel.style.height = '100%';
  55. DCstoragePanel.style.width = '250px';
  56. DCstoragePanel.style.backgroundColor = '#171717';
  57. DCstoragePanel.style.transition = 'right 0.3s ease';
  58. DCstoragePanel.style.zIndex = '999';
  59.  
  60. // Create the header above the button
  61. const storagePanelHeader = document.createElement('div');
  62. storagePanelHeader.innerText = 'Shapes Tools';
  63. storagePanelHeader.style.margin = '20px';
  64. storagePanelHeader.style.padding = '10px';
  65. storagePanelHeader.style.fontSize = '19px';
  66. storagePanelHeader.style.fontWeight = '550';
  67. storagePanelHeader.style.color = '#ECECEC';
  68. storagePanelHeader.style.textAlign = 'center';
  69.  
  70. // Create a divider line
  71. const dividerLine = document.createElement('div');
  72. dividerLine.style.height = '1px';
  73. dividerLine.style.backgroundColor = '#212121';
  74. dividerLine.style.margin = '10px 20px';
  75.  
  76. // Manage Lorebook Button
  77. window.openLorebookButton = document.createElement('div');
  78. window.openLorebookButton.innerHTML = `
  79. <button id="toggle-lorebook-panel"
  80. style="
  81. display: flex;
  82. align-items: center;
  83. gap: 8px;
  84. position: relative;
  85. top: 10px;
  86. right: 0px;
  87. left: 10px;
  88. padding: 7px 15px;
  89. background: transparent;
  90. color: #b0b0b0;
  91. border: none;
  92. border-radius: 8px;
  93. font-size: 16px;
  94. text-align: left;
  95. cursor: pointer;
  96. width: 90%;
  97. transition: background-color 0.1s, color 0.1s;
  98. z-index: 1001;">
  99. <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="mb-[-1px]">
  100. <path d="M6 3C4.89543 3 4 3.89543 4 5V13C4 14.1046 4.89543 15 6 15L6 3Z" fill="currentColor"></path>
  101. <path d="M7 3V15H8.18037L8.4899 13.4523C8.54798 13.1619 8.69071 12.8952 8.90012 12.6858L12.2931 9.29289C12.7644 8.82153 13.3822 8.58583 14 8.58578V3.5C14 3.22386 13.7761 3 13.5 3H7Z" fill="currentColor"></path>
  102. <path d="M11.3512 15.5297L9.73505 15.8529C9.38519 15.9229 9.07673 15.6144 9.14671 15.2646L9.46993 13.6484C9.48929 13.5517 9.53687 13.4628 9.60667 13.393L12.9996 10C13.5519 9.44771 14.4473 9.44771 14.9996 10C15.5519 10.5523 15.5519 11.4477 14.9996 12L11.6067 15.393C11.5369 15.4628 11.448 15.5103 11.3512 15.5297Z" fill="currentColor"></path>
  103. </svg>
  104. Manage Lorebook
  105. </button>
  106. `;
  107.  
  108. const lorebookButtonElement = openLorebookButton.querySelector('button');
  109. lorebookButtonElement.onmouseover = () => {
  110. lorebookButtonElement.style.backgroundColor = '#212121';
  111. lorebookButtonElement.style.color = '#ffffff';
  112. };
  113. lorebookButtonElement.onmouseout = () => {
  114. lorebookButtonElement.style.backgroundColor = 'transparent';
  115. lorebookButtonElement.style.color = '#b0b0b0';
  116. };
  117.  
  118. // Create the main panel container
  119. const lorebookPanel = document.createElement('div');
  120. lorebookPanel.id = 'lorebookManagerPanel';
  121. lorebookPanel.style.position = 'fixed';
  122. lorebookPanel.style.top = '50%';
  123. lorebookPanel.style.left = '50%';
  124. lorebookPanel.style.transform = 'translate(-50%, -50%)';
  125. // Size different for Mobile and Desktop
  126. if (window.innerWidth <= 768) {
  127. lorebookPanel.style.width = '90%';
  128. lorebookPanel.style.height = '90%';
  129. } else {
  130. lorebookPanel.style.width = '800px';
  131. lorebookPanel.style.height = '700px';
  132. }
  133. lorebookPanel.style.backgroundColor = '#2F2F2F';
  134. lorebookPanel.style.borderRadius = '20px';
  135. lorebookPanel.style.padding = '10px';
  136. lorebookPanel.style.display = 'none';
  137. lorebookPanel.style.zIndex = '1000';
  138.  
  139. // Add close button for the panel
  140. const closeButton = document.createElement('button');
  141. closeButton.innerText = '✕';
  142. closeButton.style.position = 'absolute';
  143. closeButton.style.borderRadius = '50%';
  144. closeButton.style.color = '#ffffff';
  145. closeButton.style.top = '20px';
  146. closeButton.style.right = '20px';
  147. closeButton.style.backgroundColor = 'transparent';
  148. closeButton.style.cursor = 'pointer';
  149.  
  150. closeButton.addEventListener('mouseenter', () => {
  151. closeButton.style.backgroundColor = '#676767';
  152. });
  153. closeButton.addEventListener('mouseleave', () => {
  154. closeButton.style.backgroundColor = 'transparent';
  155. });
  156.  
  157. // Hide the Lorebook Panel
  158. closeButton.addEventListener('click', () => {
  159. lorebookPanel.style.display = 'none';
  160. });
  161.  
  162. // Open the Lorebook Panel
  163. openLorebookButton.addEventListener('click', () => {
  164. lorebookPanel.style.display = lorebookPanel.style.display === 'none' ? 'block' : 'none';
  165. loadProfileEntries();
  166. });
  167.  
  168. // Profiles Title
  169. const profileslabel = document.createElement('div');
  170. profileslabel.textContent = 'Profiles';
  171. profileslabel.style.color = '#dddddd';
  172. profileslabel.style.fontSize = '14px';
  173. profileslabel.style.marginBottom = '5px';
  174. profileslabel.style.marginLeft = '3px';
  175. profileslabel.style.marginTop = '5px';
  176. profileslabel.style.fontSize = '20px';
  177. profileslabel.style.fontWeight = '550';
  178.  
  179. // Profile Management Panel
  180. const lorebookProfilePanel = document.createElement('div');
  181. lorebookProfilePanel.id = 'lorebookProfilePanel';
  182. lorebookProfilePanel.style.float = 'left';
  183. lorebookProfilePanel.style.width = '20%';
  184. lorebookProfilePanel.style.borderRight = '0.5px solid #444444';
  185. lorebookProfilePanel.style.height = '93%';
  186.  
  187. // Create the profile list container
  188. const profileList = document.createElement('div');
  189. profileList.id = 'profileList';
  190. profileList.style.height = '95%';
  191. profileList.style.color = 'white';
  192. profileList.style.overflowY = 'auto';
  193.  
  194. // Add Profiles Button
  195. const addProfileButton = document.createElement('button');
  196. addProfileButton.innerText = 'Add Profile';
  197. addProfileButton.style.padding = '8px';
  198. addProfileButton.style.border = '0.2px solid #4E4E4E';
  199. addProfileButton.style.backgroundColor = 'transparent';
  200. addProfileButton.style.color = '#fff';
  201. addProfileButton.style.borderRadius = '20px';
  202. addProfileButton.style.width = '90%';
  203. addProfileButton.style.cursor = 'pointer';
  204.  
  205. // Mouseover Effect for Add Profiles Button
  206. addProfileButton.onmouseover = () => {
  207. addProfileButton.style.backgroundColor = '#424242';
  208. };
  209. addProfileButton.onmouseout = () => {
  210. addProfileButton.style.backgroundColor = 'transparent';
  211. };
  212.  
  213. addProfileButton.addEventListener('click', () => {
  214. const profileName = prompt('Enter profile name:');
  215. if (profileName) {
  216. const profileKey = `lorebook.profile:${profileName}`;
  217.  
  218. // Create a list of keys from localStorage that match the prefix 'lorebook.profile:'
  219. const existingKeys = Object.keys(localStorage)
  220. .filter(key => key.startsWith('lorebook.profile:'))
  221. .map(key => key.toLowerCase()); // Convert all keys to lowercase for case-insensitive check
  222.  
  223. if (!existingKeys.includes(profileKey.toLowerCase())) {
  224. localStorage.setItem(profileKey, JSON.stringify({}));
  225. loadProfiles();
  226. } else {
  227. alert('Profile already exists.');
  228. }
  229. }
  230. });
  231.  
  232.  
  233. // Profile Selection Functionality
  234. function loadProfiles() {
  235. profileList.innerHTML = '';
  236. Object.keys(localStorage).forEach(profileKey => {
  237. if (profileKey.startsWith('lorebook.profile:')) {
  238. const profileName = profileKey.replace('lorebook.profile:', '');
  239. const profileItem = document.createElement('div');
  240. profileItem.innerText = profileName;
  241. profileItem.style.padding = '5px';
  242. profileItem.style.marginBottom = '5px';
  243. profileItem.style.cursor = 'pointer';
  244. profileItem.style.backgroundColor = profileName === getCurrentProfile() ? '#424242' : '#2F2F2F';
  245. profileItem.style.borderRadius = '5px';
  246. profileItem.style.width = '90%';
  247. profileItem.style.position = 'relative';
  248.  
  249. profileItem.addEventListener('click', () => {
  250. setCurrentProfile(profileName);
  251. loadProfiles();
  252. loadProfileEntries();
  253. });
  254.  
  255. const removeButton = document.createElement('button');
  256. removeButton.innerText = '✕';
  257. removeButton.style.position = 'absolute';
  258. removeButton.style.top = '3px';
  259. removeButton.style.right = '10px';
  260. removeButton.style.cursor = 'pointer';
  261. removeButton.style.backgroundColor = 'transparent';
  262. removeButton.style.color = 'white';
  263. removeButton.addEventListener('click', (e) => {
  264. e.stopPropagation();
  265. localStorage.removeItem(profileKey);
  266. if (profileName === getCurrentProfile()) {
  267. setCurrentProfile(null);
  268. }
  269. loadProfiles();
  270. loadProfileEntries();
  271. });
  272.  
  273. profileItem.appendChild(removeButton);
  274. profileList.appendChild(profileItem);
  275. }
  276. });
  277. }
  278.  
  279. // Lorebook Entries Title
  280. const lorebookEntriesTitle = document.createElement('h3');
  281. lorebookEntriesTitle.innerText = 'Manage Lorebook Entries';
  282. lorebookEntriesTitle.style.fontWeight = 'normal';
  283. lorebookEntriesTitle.style.color = '#ffffff';
  284. lorebookEntriesTitle.style.textAlign = 'left';
  285. lorebookEntriesTitle.style.fontSize = '24px';
  286. lorebookEntriesTitle.style.marginTop = '20px';
  287. lorebookEntriesTitle.style.position = 'relative';
  288. lorebookEntriesTitle.style.marginLeft = '23%';
  289. lorebookEntriesTitle.style.marginBottom = '15px';
  290. lorebookEntriesTitle.style.fontWeight = '550';
  291.  
  292. // Profile Entries List
  293. const profileEntriesList = document.createElement('div');
  294. profileEntriesList.id = 'profileEntriesList';
  295. profileEntriesList.style.marginTop = '20px';
  296. profileEntriesList.style.height = '48%';
  297. // Check if the device is mobile
  298. if (window.innerWidth <= 768) {
  299. profileEntriesList.style.height = '30%';
  300. } else {
  301. profileEntriesList.style.height = '48%';
  302. }
  303. profileEntriesList.style.overflowY = 'auto';
  304.  
  305. // Header for Inputs
  306. const entrieslabel = document.createElement('div');
  307. entrieslabel.textContent = 'Enter keys and description:';
  308. entrieslabel.style.color = '#dddddd';
  309. entrieslabel.style.fontSize = '14px';
  310. entrieslabel.style.marginBottom = '5px';
  311. entrieslabel.style.marginTop = '5px';
  312. entrieslabel.style.marginLeft = '23%';
  313.  
  314. // Create key-value input fields
  315. const inputContainer = document.createElement('div');
  316. inputContainer.id = 'inputContainer';
  317. inputContainer.style.marginTop = '10px';
  318. inputContainer.style.display = 'flex';
  319. inputContainer.style.flexDirection = 'column';
  320. inputContainer.style.alignItems = 'center';
  321. inputContainer.style.margin = '0 auto';
  322.  
  323.  
  324. const lorebookKeyInput = document.createElement('input');
  325. lorebookKeyInput.type = 'text';
  326. lorebookKeyInput.placeholder = 'Entry Keywords (comma-separated)';
  327. lorebookKeyInput.style.width = '90%';
  328. lorebookKeyInput.style.marginBottom = '5px';
  329. lorebookKeyInput.style.padding = '10px';
  330. lorebookKeyInput.style.border = '1px solid #444444';
  331. lorebookKeyInput.style.borderRadius = '8px';
  332. lorebookKeyInput.style.backgroundColor = '#1e1e1e';
  333. lorebookKeyInput.style.color = '#dddddd';
  334.  
  335.  
  336. const lorebookValueInput = document.createElement('textarea');
  337. lorebookValueInput.placeholder = ' ';
  338. lorebookValueInput.style.width = '90%';
  339. lorebookValueInput.style.marginBottom = '5px';
  340. lorebookValueInput.style.padding = '10px';
  341. lorebookValueInput.style.border = '1px solid #444444';
  342. lorebookValueInput.style.borderRadius = '8px';
  343. lorebookValueInput.style.backgroundColor = '#1e1e1e';
  344. lorebookValueInput.style.color = '#dddddd';
  345. lorebookValueInput.style.height = '100px';
  346. lorebookValueInput.style.resize = 'vertical';
  347. lorebookValueInput.maxLength = 1000;
  348. lorebookValueInput.style.overflow = 'auto';
  349.  
  350. const charCounter = document.createElement('div');
  351. charCounter.style.color = '#dddddd';
  352. charCounter.style.fontSize = '12px';
  353. charCounter.style.marginTop = '0px';
  354. charCounter.style.marginBottom = '15px';
  355. charCounter.style.textAlign = 'right';
  356. charCounter.style.marginRight = '-87%';
  357. charCounter.style.color = 'grey';
  358. charCounter.textContent = `0/${lorebookValueInput.maxLength}`;
  359.  
  360. // Update the counter as the user types
  361. lorebookValueInput.addEventListener('input', () => {
  362. charCounter.textContent = `${lorebookValueInput.value.length}/${lorebookValueInput.maxLength}`;
  363. });
  364.  
  365. // Save Entry button, also important for editing
  366. const lorebookSaveButton = document.createElement('button');
  367. lorebookSaveButton.innerText = 'Add Entry';
  368. lorebookSaveButton.style.padding = '10px 20px';
  369. lorebookSaveButton.style.border = '0.2px solid #4E4E4E';
  370. lorebookSaveButton.style.backgroundColor = '#2F2F2F';
  371. lorebookSaveButton.style.color = '#fff';
  372. lorebookSaveButton.style.borderRadius = '50px';
  373. lorebookSaveButton.style.cursor = 'pointer';
  374. lorebookSaveButton.style.width = '95%';
  375.  
  376. // Append all Elements
  377. document.body.appendChild(arrowButton);
  378. document.body.appendChild(DCstoragePanel);
  379. DCstoragePanel.appendChild(storagePanelHeader);
  380. DCstoragePanel.appendChild(dividerLine);
  381. document.body.appendChild(lorebookPanel);
  382. lorebookPanel.appendChild(closeButton);
  383. lorebookPanel.appendChild(profileslabel);
  384. lorebookPanel.appendChild(lorebookProfilePanel);
  385. lorebookProfilePanel.appendChild(profileList);
  386. lorebookProfilePanel.appendChild(addProfileButton);
  387. lorebookPanel.appendChild(lorebookEntriesTitle);
  388. lorebookPanel.appendChild(profileEntriesList);
  389. lorebookPanel.appendChild(entrieslabel);
  390. lorebookPanel.appendChild(inputContainer);
  391. inputContainer.appendChild(lorebookKeyInput);
  392. inputContainer.appendChild(lorebookValueInput);
  393. inputContainer.appendChild(charCounter);
  394. inputContainer.appendChild(lorebookSaveButton);
  395.  
  396. let isEditing = false;
  397. let editingKey = '';
  398.  
  399. lorebookSaveButton.addEventListener('click', () => {
  400. const key = lorebookKeyInput.value.trim().toLowerCase(); // Ensure key is always saved as lowercase
  401. const value = lorebookValueInput.value;
  402. const currentProfile = getCurrentProfile();
  403.  
  404. if (key && currentProfile) {
  405. const profileKey = `${currentProfile}.lorebook:${key}`;
  406. const formattedValue = `<[Lorebook: ${key}] ${value}>`;
  407.  
  408. // Check for duplicate keys (case-insensitive) and prevent keys that partially match an existing key
  409. const isDuplicateKey = Object.keys(localStorage).some(storageKey => {
  410. const normalizedStorageKey = storageKey.toLowerCase();
  411. const normalizedCurrentProfile = currentProfile.toLowerCase();
  412. const currentKey = normalizedStorageKey.replace(`${normalizedCurrentProfile}.lorebook:`.toLowerCase(), '');
  413. return (
  414. (currentKey === key || currentKey.split(',').includes(key)) &&
  415. (!isEditing || editingKey.toLowerCase() !== currentKey)
  416. );
  417. });
  418.  
  419. // Enforce uniqueness for edits as well
  420. if (isDuplicateKey) {
  421. alert('The key is already used in an existing entry (case-insensitive). Please use a different key.');
  422. return;
  423. }
  424.  
  425. // Remove the old key if editing and key has changed
  426. if (isEditing && editingKey.toLowerCase() !== key) {
  427. const oldProfileKey = `${currentProfile}.lorebook:${editingKey.toLowerCase()}`;
  428. localStorage.removeItem(oldProfileKey);
  429. }
  430.  
  431. localStorage.setItem(profileKey, formattedValue);
  432. lorebookKeyInput.value = '';
  433. lorebookValueInput.value = '';
  434. isEditing = false;
  435. editingKey = '';
  436. loadProfileEntries();
  437. } else {
  438. alert('Please select a profile and enter a key.');
  439. }
  440. });
  441.  
  442.  
  443.  
  444. function loadProfileEntries() {
  445. profileEntriesList.innerHTML = '';
  446. profileEntriesList.style.display = 'flex';
  447. profileEntriesList.style.flexDirection = 'column';
  448. profileEntriesList.style.alignItems = 'center';
  449. profileEntriesList.style.margin = '0 auto';
  450.  
  451. const currentProfile = getCurrentProfile();
  452. if (currentProfile) {
  453. Object.keys(localStorage).forEach(storageKey => {
  454. // Normalize both the profile and the storage key for case-insensitive comparison
  455. if (storageKey.toLowerCase().startsWith(`${currentProfile.toLowerCase()}.lorebook:`)) {
  456. const entryKey = storageKey.replace(new RegExp(`^${currentProfile}\.lorebook:`, 'i'), '');
  457. const entryValue = localStorage.getItem(storageKey);
  458. const displayedValue = entryValue.replace(/^<\[Lorebook:.*?\]\s*/, '').replace(/>$/, '');
  459.  
  460. const entryItem = document.createElement('div');
  461. entryItem.style.padding = '10px';
  462. entryItem.style.marginBottom = '12px';
  463. entryItem.style.borderRadius = '8px';
  464. entryItem.style.backgroundColor = '#424242';
  465. entryItem.style.position = 'relative';
  466. entryItem.style.color = 'white';
  467. entryItem.style.flexDirection = 'column';
  468. entryItem.style.width = '90%';
  469.  
  470. const keyElement = document.createElement('div');
  471. keyElement.innerText = entryKey;
  472. keyElement.style.fontWeight = 'bold';
  473. keyElement.style.marginBottom = '10px';
  474. entryItem.appendChild(keyElement);
  475.  
  476. const valueElement = document.createElement('div');
  477. valueElement.innerText = displayedValue;
  478. entryItem.appendChild(valueElement);
  479.  
  480. entryItem.addEventListener('click', () => {
  481. lorebookKeyInput.value = entryKey;
  482. lorebookValueInput.value = entryValue.replace(/^<\[Lorebook:.*?\]\s*/, '').replace(/>$/, '');
  483. isEditing = true;
  484. editingKey = entryKey;
  485. });
  486.  
  487. const removeButton = document.createElement('button');
  488. removeButton.innerText = '✕';
  489. removeButton.style.position = 'absolute';
  490. removeButton.style.top = '10px';
  491. removeButton.style.right = '10px';
  492. removeButton.style.cursor = 'pointer';
  493. removeButton.style.backgroundColor = 'transparent';
  494. removeButton.style.color = 'white';
  495. removeButton.addEventListener('click', (event) => {
  496. event.stopPropagation();
  497. localStorage.removeItem(storageKey);
  498. loadProfileEntries();
  499. });
  500.  
  501. entryItem.appendChild(removeButton);
  502. profileEntriesList.appendChild(entryItem);
  503. }
  504. });
  505. }
  506. }
  507.  
  508. // Utility functions to manage profiles and local storage
  509. function getCurrentProfile() {
  510. // Return the current profile name in its original case, but convert to lower case for comparisons
  511. const selectedProfile = localStorage.getItem('selectedProfile.lorebook');
  512. return selectedProfile ? selectedProfile : null;
  513. }
  514.  
  515. function setCurrentProfile(profileName) {
  516. localStorage.setItem('selectedProfile.lorebook', profileName);
  517. }
  518.  
  519. loadProfiles();
  520.  
  521. }
  522.  
  523. // ================================================================ IMPORT/EXPORT ===============================================================
  524. function ImportExportScript() {
  525. // Create buttons to trigger export and import
  526. window.exportButton = document.createElement('div');
  527. exportButton.innerHTML = `
  528. <button id="toggle-export-button"
  529. style="
  530. position: relative;
  531. top: 10px;
  532. right: 0px;
  533. left: 10px;
  534. padding: 7px 15px;
  535. background: transparent;
  536. color: #b0b0b0;
  537. border: none;
  538. border-radius: 8px;
  539. font-size: 16px;
  540. text-align: left;
  541. cursor: pointer;
  542. width: 90%;
  543. transition: background-color 0.1s, color 0.1s;
  544. z-index: 1001;
  545. display: flex;
  546. align-items: center;">
  547. <svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 8px;">
  548. <path d="M12 5L11.2929 4.29289L12 3.58579L12.7071 4.29289L12 5ZM13 14C13 14.5523 12.5523 15 12 15C11.4477 15 11 14.5523 11 14L13 14ZM6.29289 9.29289L11.2929 4.29289L12.7071 5.70711L7.70711 10.7071L6.29289 9.29289ZM12.7071 4.29289L17.7071 9.29289L16.2929 10.7071L11.2929 5.70711L12.7071 4.29289ZM13 5L13 14L11 14L11 5L13 5Z" fill="#B0B0B0"></path>
  549. <path d="M5 16L5 17C5 18.1046 5.89543 19 7 19L17 19C18.1046 19 19 18.1046 19 17V16" stroke="#B0B0B0" stroke-width="2"></path>
  550. </svg>
  551. Export Data
  552. </button>
  553. `;
  554.  
  555.  
  556. window.exportButton.onmouseover = () => {
  557. exportButton.querySelector('button').style.backgroundColor = '#212121';
  558. exportButton.querySelector('button').style.color = '#ffffff';
  559. };
  560. exportButton.onmouseout = () => {
  561. exportButton.querySelector('button').style.backgroundColor = 'transparent';
  562. exportButton.querySelector('button').style.color = '#b0b0b0';
  563. };
  564. //DCstoragePanel.appendChild(exportButton);
  565.  
  566. window.importButton = document.createElement('div');
  567. importButton.innerHTML = `
  568. <button id="toggle-import-button"
  569. style="
  570. position: relative;
  571. top: 10px;
  572. right: 0px;
  573. left: 10px;
  574. padding: 7px 15px;
  575. background: transparent;
  576. color: #b0b0b0;
  577. border: none;
  578. border-radius: 8px;
  579. font-size: 16px;
  580. text-align: left;
  581. cursor: pointer;
  582. width: 90%;
  583. transition: background-color 0.1s, color 0.1s;
  584. z-index: 1001;
  585. display: flex;
  586. align-items: center;
  587. gap: 8px;">
  588. <svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  589. <path d="M12 14L11.2929 14.7071L12 15.4142L12.7071 14.7071L12 14ZM13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44771 11 5L13 5ZM6.29289 9.70711L11.2929 14.7071L12.7071 13.2929L7.70711 8.29289L6.29289 9.70711ZM12.7071 14.7071L17.7071 9.70711L16.2929 8.29289L11.2929 13.2929L12.7071 14.7071ZM13 14L13 5L11 5L11 14L13 14Z" fill="#B0B0B0"></path>
  590. <path d="M5 16L5 17C5 18.1046 5.89543 19 7 19L17 19C18.1046 19 19 18.1046 19 17V16" stroke="#B0B0B0" stroke-width="2"></path>
  591. </svg>
  592. Import Data
  593. </button>
  594. `;
  595.  
  596. importButton.onmouseover = () => {
  597. importButton.querySelector('button').style.backgroundColor = '#212121';
  598. importButton.querySelector('button').style.color = '#ffffff';
  599. };
  600. importButton.onmouseout = () => {
  601. importButton.querySelector('button').style.backgroundColor = 'transparent';
  602. importButton.querySelector('button').style.color = '#b0b0b0';
  603. };
  604. //DCstoragePanel.appendChild(importButton);
  605.  
  606. // Export specific localStorage entries
  607. exportButton.addEventListener('click', () => {
  608. const filteredData = {};
  609. for (const key in localStorage) {
  610. if (localStorage.hasOwnProperty(key)) {
  611. if (key.startsWith('events') || key.includes('lorebook') || key.includes('Rule') || key.includes('notifier')) {
  612. filteredData[key] = localStorage.getItem(key);
  613. }
  614. }
  615. }
  616. const data = JSON.stringify(filteredData);
  617. const blob = new Blob([data], {type: 'application/json'});
  618. const url = URL.createObjectURL(blob);
  619.  
  620. const a = document.createElement('a');
  621. a.href = url;
  622. a.download = 'localStorage_filtered.json';
  623. document.body.appendChild(a);
  624. a.click();
  625. document.body.removeChild(a);
  626. });
  627.  
  628. // Import localStorage
  629. importButton.addEventListener('click', () => {
  630. const input = document.createElement('input');
  631. input.type = 'file';
  632. input.accept = 'application/json';
  633. input.addEventListener('change', (event) => {
  634. const file = event.target.files[0];
  635. if (!file) {
  636. return;
  637. }
  638.  
  639. const reader = new FileReader();
  640. reader.onload = (e) => {
  641. try {
  642. const importedData = JSON.parse(e.target.result);
  643. for (const key in importedData) {
  644. localStorage.setItem(key, importedData[key]);
  645. }
  646. alert('localStorage has been successfully imported.');
  647. } catch (err) {
  648. alert('Failed to import localStorage: ' + err.message);
  649. }
  650. };
  651. reader.readAsText(file);
  652. });
  653. input.click();
  654. });
  655. }
  656.  
  657. // =================================================================== EVENTS ===================================================================
  658. function EventsScript() {
  659.  
  660. window.eventsButton = document.createElement('div');
  661. window.eventsButton.innerHTML = `
  662. <button id="toggle-events-panel"
  663. style="
  664. position: relative;
  665. top: 10px;
  666. right: 0px;
  667. left: 10px;
  668. padding: 7px 15px;
  669. display: flex;
  670. align-items: center;
  671.  
  672. text-align: left;
  673. background: transparent;
  674. color: #b0b0b0;
  675. border: none;
  676. border-radius: 8px;
  677. font-size: 16px;
  678. cursor: pointer;
  679. width: 90%;
  680. transition: background-color 0.1s, color 0.1s;
  681. z-index: 1001;">
  682. <svg fill="#B0B0B0" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" xml:space="preserve" width="20px" height="20px" style="padding-right: 5px; margin-left: 1px;">
  683.  
  684. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  685. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  686. <g id="SVGRepo_iconCarrier">
  687. <path d="M5,2v2H4C2.9,4,2,4.9,2,6v11c0,1.1,0.9,2,2,2h6.8c1.8-1.8,0,0,2-2H4V8h12v5.9c1.6-1.6,0.2-0.2,2-2V6c0-1.1-0.9-2-2-2h-1V2 h-2v2H7V2H5z M10,9.2l-0.8,2L7,11.4l1.6,1.4l-0.5,2.1l1.8-1.1l1.8,1.1l-0.5-2.1l1.6-1.4l-2.2-0.2L10,9.2z M20.5,12 c-0.1,0-0.3,0.1-0.4,0.2L19.3,13l2,2l0.8-0.8c0.2-0.2,0.2-0.6,0-0.7l-1.3-1.3C20.8,12,20.6,12,20.5,12z M18.8,13.5L12.3,20v2h2 l6.5-6.5L18.8,13.5"></path>
  688. </g>
  689. </svg>
  690. Manage Events
  691. </button>
  692. `;
  693.  
  694.  
  695. let manageEventsButton = window.eventsButton.querySelector('#toggle-events-panel');
  696. manageEventsButton.onclick = openProfilePanel;
  697.  
  698. const eventsbuttonElement = eventsButton.querySelector('button');
  699. eventsbuttonElement.onmouseover = () => {
  700. eventsbuttonElement.style.backgroundColor = '#212121';
  701. eventsbuttonElement.style.color = '#ffffff';
  702. };
  703. eventsbuttonElement.onmouseout = () => {
  704. eventsbuttonElement.style.backgroundColor = 'transparent';
  705. eventsbuttonElement.style.color = '#b0b0b0';
  706. };
  707.  
  708. function openProfilePanel() {
  709.  
  710. if (document.querySelector('#eventsProfilePanel')) {
  711. return;
  712. }
  713.  
  714. // Create profile management panel
  715. const eventsProfilePanel = document.createElement('div');
  716. eventsProfilePanel.id = 'eventsProfilePanel';
  717. eventsProfilePanel.style.position = 'fixed';
  718. eventsProfilePanel.style.top = '50%';
  719. eventsProfilePanel.style.left = '50%';
  720. eventsProfilePanel.style.transform = 'translate(-50%, -50%)';
  721. // Size different for Mobile and Desktop
  722. if (window.innerWidth <= 768) {
  723. eventsProfilePanel.style.width = '90%';
  724. eventsProfilePanel.style.height = '90%';
  725. } else {
  726. eventsProfilePanel.style.width = '800px';
  727. eventsProfilePanel.style.height = '700px';
  728. }
  729. //eventsProfilePanel.style.width = '800px';
  730. //eventsProfilePanel.style.height = '700px';
  731. eventsProfilePanel.style.backgroundColor = '#2F2F2F';
  732. eventsProfilePanel.style.color = 'white';
  733. eventsProfilePanel.style.borderRadius = '20px';
  734. eventsProfilePanel.style.padding = '20px';
  735. eventsProfilePanel.style.zIndex = '1000';
  736. eventsProfilePanel.style.display = 'flex';
  737. eventsProfilePanel.style.flexDirection = 'row';
  738.  
  739. // Create close button for profilePanel
  740. const closeButton = document.createElement('button');
  741. closeButton.style.position = 'absolute';
  742. closeButton.style.top = '15px';
  743. closeButton.style.right = '15px';
  744. closeButton.style.width = '30px';
  745. closeButton.style.height = '30px';
  746. closeButton.style.backgroundColor = 'transparent';
  747. closeButton.style.color = '#ffffff';
  748. closeButton.style.border = 'none';
  749. closeButton.style.borderRadius = '50%';
  750. closeButton.style.cursor = 'pointer';
  751. closeButton.style.display = 'flex';
  752. closeButton.style.alignItems = 'center';
  753. closeButton.style.zIndex = '1001';
  754. closeButton.style.justifyContent = 'center';
  755. closeButton.style.transition = 'background-color 0.2s ease';
  756. closeButton.style.boxSizing = 'border-box';
  757.  
  758. // Create span for the '✕' character
  759. const closeIcon = document.createElement('span');
  760. closeIcon.innerText = '✕';
  761. closeIcon.style.fontSize = '16px';
  762. closeIcon.style.position = 'relative';
  763. closeIcon.style.top = '-1px';
  764.  
  765. // Hover effect
  766. closeButton.addEventListener('mouseenter', () => {
  767. closeButton.style.backgroundColor = '#676767';
  768. });
  769.  
  770. closeButton.addEventListener('mouseleave', () => {
  771. closeButton.style.backgroundColor = 'transparent';
  772. });
  773.  
  774. // Close button action
  775. closeButton.onclick = function() {
  776. document.body.removeChild(eventsProfilePanel);
  777. };
  778.  
  779. // Create profile list container
  780. const profileListContainer = document.createElement('div');
  781. profileListContainer.style.flex = '0.50';
  782. profileListContainer.style.marginRight = '20px';
  783. profileListContainer.style.paddingRight = '20px';
  784. profileListContainer.style.display = 'flex';
  785. profileListContainer.style.flexDirection = 'column';
  786. profileListContainer.style.borderRight = '1px solid #444444';
  787. profileListContainer.style.overflowY = 'auto';
  788. profileListContainer.style.maxHeight = '660px';
  789.  
  790. // Create header for profile list
  791. const profileListHeader = document.createElement('h4');
  792. profileListHeader.innerText = 'Profiles';
  793. profileListHeader.style.marginBottom = '10px';
  794.  
  795. // Create profile list
  796. const profileList = document.createElement('ul');
  797. profileList.style.overflowY = 'auto';
  798. profileList.style.height = '600px';
  799.  
  800.  
  801. // Create button to add profile
  802. const addProfileButton = document.createElement('button');
  803. addProfileButton.innerText = 'Add Profile';
  804. addProfileButton.style.padding = '8px';
  805. addProfileButton.style.border = '0.2px solid #4E4E4E';
  806.  
  807. addProfileButton.style.backgroundColor = 'transparent';
  808. addProfileButton.style.color = '#fff';
  809. addProfileButton.style.borderRadius = '20px';
  810. addProfileButton.style.cursor = 'pointer';
  811.  
  812. addProfileButton.onmouseover = () => {
  813. addProfileButton.style.backgroundColor = '#424242';
  814. };
  815. addProfileButton.onmouseout = () => {
  816. addProfileButton.style.backgroundColor = 'transparent';
  817. };
  818. addProfileButton.onclick = function() {
  819. openAddProfileDialog();
  820. };
  821.  
  822. // Create key-value input container
  823. const keyValueContainer = document.createElement('div');
  824. keyValueContainer.style.flex = '2.3';
  825. keyValueContainer.style.display = 'flex';
  826. keyValueContainer.style.flexDirection = 'column';
  827. keyValueContainer.style.gap = '10px';
  828.  
  829. // Create entries list
  830. const entriesList = document.createElement('div');
  831. entriesList.style.overflowY = 'auto';
  832. entriesList.style.height = '340px';
  833. entriesList.style.width = '100%';
  834. entriesList.style.paddingRight = '20px';
  835. entriesList.style.borderCollapse = 'collapse';
  836. entriesList.style.display = 'block';
  837. entriesList.style.overflowY = 'auto';
  838.  
  839. // Create a header above the headerRow
  840. const manageEventsHeader = document.createElement('h2');
  841. manageEventsHeader.innerText = 'Manage Event List';
  842. manageEventsHeader.style.marginBottom = '-10px';
  843.  
  844. closeButton.appendChild(closeIcon);
  845. eventsProfilePanel.appendChild(closeButton);
  846. profileListContainer.appendChild(profileListHeader);
  847. profileListContainer.appendChild(profileList);
  848. profileListContainer.appendChild(addProfileButton);
  849. keyValueContainer.appendChild(manageEventsHeader);
  850.  
  851. // Create table header
  852. const headerRow = document.createElement('tr');
  853. const headers = ['Key', 'Value', '%', 'Time', ''];
  854. headers.forEach(headerText => {
  855. const header = document.createElement('th');
  856. header.innerText = headerText;
  857. header.style.padding = '5px';
  858. header.style.textAlign = 'left';
  859. if (headerText === 'Value') {
  860. header.style.width = '60%';
  861. } else if (headerText === 'Key') {
  862. header.style.width = '15%';
  863. } else if (headerText === '%' || headerText === 'Time') {
  864. header.style.width = '10%';
  865. } else if (headerText === '') {
  866. header.style.width = '5%';
  867. }
  868. headerRow.appendChild(header);
  869. });
  870.  
  871. const headerContainer = document.createElement('div');
  872. headerContainer.style.position = 'sticky';
  873. headerContainer.style.top = '0';
  874. headerContainer.style.backgroundColor = '#2F2F2F';
  875. headerContainer.style.zIndex = '1';
  876. headerContainer.appendChild(headerRow);
  877. entriesList.appendChild(headerContainer);
  878. entriesList.style.position = 'sticky';
  879. entriesList.style.top = '0';
  880. entriesList.style.backgroundColor = '#2F2F2F';
  881. // Create a separate header for the entries list
  882. const entriesHeaderContainer = document.createElement('div');
  883. entriesHeaderContainer.style.position = 'sticky';
  884. entriesHeaderContainer.style.top = '0';
  885. entriesHeaderContainer.style.backgroundColor = '#2F2F2F';
  886. entriesHeaderContainer.style.zIndex = '1';
  887. const entriesHeader = document.createElement('div');
  888. entriesHeader.style.display = 'flex';
  889. entriesHeader.style.padding = '5px 0';
  890. entriesHeader.style.borderBottom = '1px solid #444444';
  891. headers.forEach(headerText => {
  892. const header = document.createElement('div');
  893. header.innerText = headerText;
  894. header.style.padding = '5px';
  895. header.style.textAlign = 'left'; // Align headers to the left
  896. if (headerText === 'Value') {
  897. header.style.width = '57%';
  898. } else if (headerText === 'Key') {
  899. header.style.width = '15%';
  900. } else if (headerText === '%' || headerText === 'Time') {
  901. header.style.width = '7%';
  902. } else if (headerText === '') {
  903. header.style.width = '5%';
  904. }
  905. entriesHeader.appendChild(header);
  906. });
  907. entriesHeaderContainer.appendChild(entriesHeader);
  908. keyValueContainer.appendChild(entriesHeaderContainer);
  909. keyValueContainer.appendChild(entriesList);
  910.  
  911. // Create container for probability and time range inputs
  912. const probTimeContainer = document.createElement('div');
  913. probTimeContainer.style.display = 'flex';
  914. probTimeContainer.style.gap = '10px';
  915. probTimeContainer.style.marginBottom = '-4px';
  916.  
  917. // Create probability label and input
  918. const probabilityContainer = document.createElement('div');
  919. probabilityContainer.style.display = 'flex';
  920. probabilityContainer.style.flexDirection = 'column';
  921. probabilityContainer.style.width = '30%'; // Adjust width to fit the row better
  922.  
  923. const probabilityLabel = document.createElement('div');
  924. probabilityLabel.innerText = 'Event Probability';
  925. probabilityLabel.style.color = 'white';
  926. probabilityLabel.style.marginBottom = '0px';
  927. probabilityContainer.appendChild(probabilityLabel);
  928.  
  929. const probabilityInputContainer = document.createElement('div');
  930. probabilityInputContainer.style.display = 'flex';
  931. probabilityInputContainer.style.alignItems = 'center';
  932.  
  933. const probabilityInput = document.createElement('input');
  934. probabilityInput.type = 'number';
  935. probabilityInput.placeholder = '0-100';
  936. probabilityInput.style.backgroundColor = '#1E1E1E';
  937. probabilityInput.style.color = 'white';
  938. probabilityInput.style.border = '1px solid #444444';
  939. probabilityInput.style.borderRadius = '5px';
  940. probabilityInput.style.padding = '5px';
  941. probabilityInput.style.width = '85%';
  942. probabilityInput.style.marginRight = '5px';
  943. probabilityInputContainer.appendChild(probabilityInput);
  944.  
  945. const probabilityPercentLabel = document.createElement('span');
  946. probabilityPercentLabel.innerText = '%';
  947. probabilityPercentLabel.style.color = 'white';
  948. probabilityInputContainer.appendChild(probabilityPercentLabel);
  949.  
  950. probabilityContainer.appendChild(probabilityInputContainer);
  951. probTimeContainer.appendChild(probabilityContainer);
  952.  
  953. // Create time range label and input
  954. const timeRangeContainer = document.createElement('div');
  955. timeRangeContainer.style.display = 'flex';
  956. timeRangeContainer.style.flexDirection = 'column';
  957. timeRangeContainer.style.width = '30%'; // Adjust width to fit the row better
  958.  
  959. const timeRangeLabel = document.createElement('div');
  960. timeRangeLabel.innerText = 'Time Range';
  961. timeRangeLabel.style.color = 'white';
  962. timeRangeLabel.style.marginBottom = '0px';
  963. timeRangeContainer.appendChild(timeRangeLabel);
  964.  
  965. const timeRangeInputContainer = document.createElement('div');
  966. timeRangeInputContainer.style.display = 'flex';
  967. timeRangeInputContainer.style.alignItems = 'center';
  968.  
  969. const timeRangeInput = document.createElement('input');
  970. timeRangeInput.type = 'text';
  971. timeRangeInput.placeholder = '0-24';
  972. timeRangeInput.value = '0-24'; // Set default value
  973.  
  974. timeRangeInput.style.backgroundColor = '#1E1E1E';
  975. timeRangeInput.style.color = 'white';
  976. timeRangeInput.style.border = '1px solid #444444';
  977. timeRangeInput.style.borderRadius = '5px';
  978. timeRangeInput.style.padding = '5px';
  979. timeRangeInput.style.width = '85%';
  980. timeRangeInput.style.marginRight = '5px';
  981.  
  982. timeRangeInput.addEventListener('blur', () => {
  983. const timeValue = timeRangeInput.value.trim();
  984. const timeRegex = /^([0-9]|1[0-9]|2[0-3])-(?:[0-9]|1[0-9]|2[0-3])$/;
  985. if (!timeRegex.test(timeValue)) {
  986. alert('Please enter a valid time range between 0-23, e.g., "8-16" or "0-24". Defaulting to "0-24".');
  987. timeRangeInput.value = '0-24';
  988. }
  989. });
  990.  
  991. timeRangeInputContainer.appendChild(timeRangeInput);
  992.  
  993. const timeRangeUnitLabel = document.createElement('span');
  994. timeRangeUnitLabel.innerText = 'h';
  995. timeRangeUnitLabel.style.color = 'white';
  996. timeRangeInputContainer.appendChild(timeRangeUnitLabel);
  997.  
  998. timeRangeContainer.appendChild(timeRangeInputContainer);
  999. probTimeContainer.appendChild(timeRangeContainer);
  1000.  
  1001. // Create overall probability label and input
  1002. const overallProbabilityContainer = document.createElement('div');
  1003. overallProbabilityContainer.style.display = 'flex';
  1004. overallProbabilityContainer.style.flexDirection = 'column';
  1005. overallProbabilityContainer.style.width = '30%'; // Adjust width to fit the row better
  1006.  
  1007. const overallProbabilityLabel = document.createElement('div');
  1008. overallProbabilityLabel.innerText = 'Overall Probability';
  1009. overallProbabilityLabel.style.color = 'white';
  1010. overallProbabilityLabel.style.marginBottom = '0px';
  1011. overallProbabilityContainer.appendChild(overallProbabilityLabel);
  1012.  
  1013. const overallProbabilityInputContainer = document.createElement('div');
  1014. overallProbabilityInputContainer.style.display = 'flex';
  1015. overallProbabilityInputContainer.style.alignItems = 'center';
  1016.  
  1017. const overallProbabilityInput = document.createElement('input');
  1018. overallProbabilityInput.type = 'number';
  1019. overallProbabilityInput.placeholder = '0-100';
  1020. overallProbabilityInput.style.backgroundColor = '#202530';
  1021. overallProbabilityInput.style.color = 'white';
  1022. overallProbabilityInput.style.border = '1px solid #444444';
  1023. overallProbabilityInput.style.borderRadius = '5px';
  1024. overallProbabilityInput.style.padding = '5px';
  1025. overallProbabilityInput.style.marginRight = '5px';
  1026. overallProbabilityInput.style.width = '85%';
  1027.  
  1028. // Load the existing overall probability value if set
  1029. const savedProbability = localStorage.getItem('events.probability');
  1030. if (savedProbability) {
  1031. overallProbabilityInput.value = savedProbability;
  1032. }
  1033.  
  1034. // Add event listener to save the value to localStorage when changed
  1035. overallProbabilityInput.addEventListener('input', () => {
  1036. const probabilityValue = overallProbabilityInput.value.trim();
  1037. if (probabilityValue !== '' && probabilityValue >= 0 && probabilityValue <= 100) {
  1038. localStorage.setItem('events.probability', probabilityValue);
  1039. } else {
  1040. alert('Please enter a valid probability between 0 and 100.');
  1041. }
  1042. });
  1043.  
  1044. overallProbabilityInputContainer.appendChild(overallProbabilityInput);
  1045.  
  1046. const overallProbabilityPercentLabel = document.createElement('span');
  1047. overallProbabilityPercentLabel.innerText = '%';
  1048. overallProbabilityPercentLabel.style.color = 'white';
  1049. overallProbabilityInputContainer.appendChild(overallProbabilityPercentLabel);
  1050.  
  1051. overallProbabilityContainer.appendChild(overallProbabilityInputContainer);
  1052. probTimeContainer.appendChild(overallProbabilityContainer);
  1053.  
  1054. // Append probability and time range container to the main keyValue container
  1055. keyValueContainer.appendChild(probTimeContainer);
  1056.  
  1057. // Create input for key
  1058. const keyInput = document.createElement('input');
  1059. keyInput.type = 'text';
  1060. keyInput.placeholder = 'Enter key';
  1061. keyInput.style.backgroundColor = '#1E1E1E';
  1062. keyInput.style.color = 'white';
  1063. keyInput.style.border = '1px solid #444444';
  1064. keyInput.style.borderRadius = '5px';
  1065. keyInput.style.padding = '5px';
  1066. keyInput.style.marginBottom = '-4px';
  1067. keyValueContainer.appendChild(keyInput);
  1068.  
  1069. // Create input for value
  1070. const valueInput = document.createElement('textarea');
  1071. valueInput.placeholder = 'Enter value';
  1072. valueInput.style.backgroundColor = '#1E1E1E';
  1073. valueInput.style.color = 'white';
  1074. valueInput.style.border = '1px solid #444444';
  1075. valueInput.style.borderRadius = '5px';
  1076. valueInput.style.padding = '5px';
  1077. valueInput.style.height = '80px';
  1078. valueInput.style.overflowWrap = 'break-word';
  1079. valueInput.style.overflow = 'auto';
  1080. valueInput.style.marginBottom = '-4px';
  1081. keyValueContainer.appendChild(valueInput);
  1082.  
  1083.  
  1084. // Create button to add key-value pair
  1085. const addEntryButton = document.createElement('button');
  1086. addEntryButton.innerText = 'Add Entry';
  1087. addEntryButton.innerText = 'Add Entry';
  1088. addEntryButton.style.padding = '10px 20px';
  1089. addEntryButton.style.border = '0.2px solid #4E4E4E';
  1090. addEntryButton.style.backgroundColor = '#2F2F2F';
  1091. addEntryButton.style.color = '#fff';
  1092. addEntryButton.style.borderRadius = '50px';
  1093. addEntryButton.style.cursor = 'pointer';
  1094.  
  1095. addEntryButton.onmouseover = () => {
  1096. addEntryButton.style.backgroundColor = '#424242';
  1097. };
  1098. addEntryButton.onmouseout = () => {
  1099. addEntryButton.style.backgroundColor = 'transparent';
  1100. };
  1101.  
  1102. // Adding Entries
  1103. let currentEditingKey = null; // This keeps track of the current key being edited
  1104.  
  1105. // Add or Edit Entry Button
  1106. addEntryButton.onclick = function () {
  1107. if (!selectedProfile) {
  1108. alert('Please select a profile before adding entries.');
  1109. return;
  1110. }
  1111.  
  1112. const key = keyInput.value.trim();
  1113. const value = `<Event: ${valueInput.value.trim()}>`;
  1114. const probability = probabilityInput.value.trim();
  1115. const timeRange = timeRangeInput.value.trim();
  1116. const fullKey = `${selectedProfile}.events:${key}`;
  1117.  
  1118. if (key && value) {
  1119. // Check if we are editing an existing entry
  1120. if (currentEditingKey) {
  1121. // Check if we are attempting to edit to a key that already exists
  1122. if (currentEditingKey !== fullKey && localStorage.getItem(fullKey)) {
  1123. alert('An entry with this key already exists. Please use a different key.');
  1124. return;
  1125. }
  1126.  
  1127. // Remove the old entry if the key has changed
  1128. if (currentEditingKey !== fullKey) {
  1129. localStorage.removeItem(currentEditingKey);
  1130. }
  1131.  
  1132. // Update or add the new entry
  1133. const entryData = {
  1134. value: value,
  1135. probability: probability || '100',
  1136. timeRange: timeRange || '0-24'
  1137. };
  1138. localStorage.setItem(fullKey, JSON.stringify(entryData));
  1139. currentEditingKey = null; // Reset the editing key
  1140.  
  1141. } else {
  1142. // If adding a new entry
  1143. if (!localStorage.getItem(fullKey)) {
  1144. const entryData = {
  1145. value: value,
  1146. probability: probability || '100',
  1147. timeRange: timeRange || '0-24'
  1148. };
  1149. localStorage.setItem(fullKey, JSON.stringify(entryData));
  1150. } else {
  1151. alert('Entry with this key already exists. Please use a different key.');
  1152. return;
  1153. }
  1154. }
  1155.  
  1156. loadEntries();
  1157.  
  1158. // Clear the input fields after adding/editing the entry
  1159. keyInput.value = '';
  1160. valueInput.value = '';
  1161. probabilityInput.value = '100';
  1162. timeRangeInput.value = '0-24';
  1163. }
  1164. };
  1165. keyValueContainer.appendChild(addEntryButton);
  1166.  
  1167. // Append containers to profilePanel
  1168. eventsProfilePanel.appendChild(profileListContainer);
  1169. eventsProfilePanel.appendChild(keyValueContainer);
  1170.  
  1171.  
  1172.  
  1173. // Load saved profiles and entries
  1174. let selectedProfile = localStorage.getItem('selectedProfile.events');
  1175.  
  1176. function loadProfiles() {
  1177. profileList.innerHTML = '';
  1178. for (let key in localStorage) {
  1179. if (key.startsWith('events.profile:')) {
  1180. const profileName = key.replace('events.profile:', '');
  1181. const listItem = document.createElement('li');
  1182. listItem.style.display = 'flex';
  1183. listItem.style.alignItems = 'center';
  1184. listItem.style.cursor = 'pointer';
  1185.  
  1186. const nameSpan = document.createElement('span');
  1187. nameSpan.innerText = profileName;
  1188. nameSpan.style.flex = '1';
  1189. listItem.appendChild(nameSpan);
  1190.  
  1191. listItem.onclick = function() {
  1192. if (selectedProfile === profileName) {
  1193. selectedProfile = null;
  1194. localStorage.setItem('selectedProfile.events', '');
  1195. listItem.style.backgroundColor = '';
  1196. loadEntries();
  1197. } else {
  1198. selectedProfile = profileName;
  1199. localStorage.setItem('selectedProfile.events', selectedProfile);
  1200. loadEntries();
  1201. highlightSelectedProfile(listItem);
  1202. }
  1203. };
  1204.  
  1205. const deleteButton = document.createElement('button');
  1206. // deleteButton.innerText = 'x';
  1207. deleteButton.style.backgroundColor = 'transparent';
  1208. deleteButton.style.borderRadius = '50%';
  1209. deleteButton.style.marginLeft = '10px';
  1210. deleteButton.style.border = 'none';
  1211. deleteButton.style.color = '#ffffff';
  1212. deleteButton.style.cursor = 'pointer';
  1213. deleteButton.style.padding = '14px';
  1214. deleteButton.style.width = '15px';
  1215. deleteButton.style.height = '15px';
  1216. deleteButton.style.display = 'flex';
  1217. deleteButton.style.alignItems = 'center';
  1218. deleteButton.style.justifyContent = 'center';
  1219.  
  1220. deleteButton.style.transition = 'background-color 0.1s';
  1221.  
  1222. // Hover effect
  1223. deleteButton.addEventListener('mouseenter', () => {
  1224. // closeButton.style.transform = 'scale(1.1)';
  1225. deleteButton.style.backgroundColor = '#676767';
  1226. });
  1227.  
  1228. deleteButton.addEventListener('mouseleave', () => {
  1229. // closeButton.style.transform = 'scale(1)';
  1230. deleteButton.style.backgroundColor = 'transparent';
  1231. });
  1232.  
  1233.  
  1234.  
  1235. const closeIcon2 = document.createElement('span');
  1236. closeIcon2.innerText = '✕';
  1237. closeIcon2.style.fontSize = '16px';
  1238. closeIcon2.style.color = '#ffffff';
  1239. closeIcon2.style.position = 'relative';
  1240. closeIcon2.style.top = '-1px'; // Adjust this value to move the character up
  1241.  
  1242. deleteButton.appendChild(closeIcon2);
  1243.  
  1244.  
  1245.  
  1246. deleteButton.onclick = function(event) {
  1247. event.stopPropagation();
  1248. localStorage.removeItem(`events.profile:${profileName}`);
  1249. if (selectedProfile === profileName) {
  1250. selectedProfile = null;
  1251. localStorage.setItem('selectedProfile.events', '');
  1252. }
  1253. loadProfiles();
  1254. loadEntries();
  1255. };
  1256. listItem.appendChild(deleteButton);
  1257.  
  1258. if (selectedProfile === profileName) {
  1259. highlightSelectedProfile(listItem);
  1260. }
  1261. profileList.appendChild(listItem);
  1262. }
  1263. }
  1264. }
  1265.  
  1266. function openAddProfileDialog() {
  1267. const dialog = document.createElement('div');
  1268. dialog.style.position = 'fixed';
  1269. dialog.style.top = '50%';
  1270. dialog.style.left = '50%';
  1271. dialog.style.transform = 'translate(-50%, -50%)';
  1272. dialog.style.backgroundColor = '#424242';
  1273. dialog.style.padding = '20px';
  1274. dialog.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
  1275. dialog.style.zIndex = '1100';
  1276.  
  1277. const input = document.createElement('input');
  1278. input.type = 'text';
  1279. input.placeholder = 'Enter profile name';
  1280. dialog.appendChild(input);
  1281.  
  1282. const addButton = document.createElement('button');
  1283. addButton.innerText = 'Add';
  1284. addButton.style.marginLeft = '10px';
  1285. addButton.onclick = function() {
  1286. const profileName = input.value.trim();
  1287. if (profileName) {
  1288. // Perform a case-insensitive check for existing profiles
  1289. const existingProfiles = Object.keys(localStorage).filter(key => key.startsWith('events.profile:'));
  1290. const profileExists = existingProfiles.some(key => key.toLowerCase() === `events.profile:${profileName.toLowerCase()}`);
  1291.  
  1292. if (!profileExists) {
  1293. localStorage.setItem(`events.profile:${profileName}`, JSON.stringify({}));
  1294. loadProfiles();
  1295. document.body.removeChild(dialog);
  1296. } else {
  1297. alert('Profile name already exists. Please choose a different name.');
  1298. }
  1299. }
  1300. };
  1301. dialog.appendChild(addButton);
  1302.  
  1303. const cancelButton = document.createElement('button');
  1304. cancelButton.innerText = 'Cancel';
  1305. cancelButton.style.marginLeft = '10px';
  1306. cancelButton.onclick = function() {
  1307. document.body.removeChild(dialog);
  1308. };
  1309. dialog.appendChild(cancelButton);
  1310.  
  1311. document.body.appendChild(dialog);
  1312. }
  1313.  
  1314.  
  1315. function loadEntries() {
  1316. entriesList.innerHTML = '';
  1317. // Add header back after clearing
  1318. if (selectedProfile) {
  1319. const prefix = `${selectedProfile}.events:`.toLowerCase();
  1320.  
  1321. for (let key in localStorage) {
  1322. if (key.toLowerCase().startsWith(prefix)) {
  1323. const entryData = JSON.parse(localStorage.getItem(key));
  1324. const row = document.createElement('div');
  1325. row.style.padding = '10px';
  1326. row.style.margin = '5px 0';
  1327. row.style.borderRadius = '10px';
  1328. row.style.marginBottom = '12px';
  1329. row.style.backgroundColor = '#424242';
  1330. row.style.display = 'flex';
  1331. row.style.alignItems = 'center';
  1332.  
  1333. // Create cells for key, value, probability, and time range
  1334. const keyCell = document.createElement('div');
  1335. keyCell.innerText = key.split(':')[1];
  1336. keyCell.style.padding = '5px';
  1337. keyCell.style.width = '15%';
  1338. row.appendChild(keyCell);
  1339.  
  1340. const valueCell = document.createElement('div');
  1341. valueCell.innerText = entryData.value.replace(/^<Event:\s*/, '').slice(0, -1); // Remove surrounding brackets
  1342. valueCell.style.padding = '5px';
  1343. valueCell.style.width = '60%';
  1344. row.appendChild(valueCell);
  1345.  
  1346. const probabilityCell = document.createElement('div');
  1347. probabilityCell.innerText = `${entryData.probability}%`;
  1348. probabilityCell.style.padding = '5px';
  1349. probabilityCell.style.width = '10%';
  1350. row.appendChild(probabilityCell);
  1351.  
  1352. const timeRangeCell = document.createElement('div');
  1353. timeRangeCell.innerText = entryData.timeRange;
  1354. timeRangeCell.style.padding = '5px';
  1355. timeRangeCell.style.width = '10%';
  1356. row.appendChild(timeRangeCell);
  1357.  
  1358. // Add remove button for each entry
  1359. const actionCell = document.createElement('div');
  1360. actionCell.style.padding = '5px';
  1361. actionCell.style.width = '5%';
  1362. const removeButton = document.createElement('button');
  1363. // removeButton.innerText = '✕';
  1364. removeButton.style.backgroundColor = 'transparent';
  1365. removeButton.style.border = 'none';
  1366. removeButton.style.cursor = 'pointer';
  1367. removeButton.style.display = 'flex';
  1368. removeButton.style.alignItems = 'center';
  1369. removeButton.style.justifyContent = 'center';
  1370. removeButton.style.width = '100%';
  1371. removeButton.style.height = '100%';
  1372. removeButton.style.transition = 'background-color 0.2s ease';
  1373. removeButton.style.borderRadius = '50%';
  1374. removeButton.style.width = '28px';
  1375. removeButton.style.height = '28px';
  1376. removeButton.style.marginLeft = '-3px';
  1377. removeButton.style.boxSizing = 'border-box';
  1378. removeButton.onclick = function() {
  1379. localStorage.removeItem(key);
  1380. loadEntries();
  1381. };
  1382.  
  1383. const closeIcon1 = document.createElement('span');
  1384. closeIcon1.innerText = '✕';
  1385. closeIcon1.style.fontSize = '16px';
  1386. closeIcon1.style.position = 'relative';
  1387. closeIcon1.style.color = '#ffffff';
  1388. closeIcon1.style.top = '-1px'; // Adjust this value to move the character up
  1389.  
  1390. // Append the span to the button
  1391. removeButton.appendChild(closeIcon1);
  1392. actionCell.appendChild(removeButton);
  1393. row.appendChild(actionCell);
  1394.  
  1395. // Hover effect
  1396. removeButton.addEventListener('mouseenter', () => {
  1397. removeButton.style.backgroundColor = '#676767';
  1398. });
  1399.  
  1400. removeButton.addEventListener('mouseleave', () => {
  1401. removeButton.style.backgroundColor = 'transparent';
  1402. });
  1403.  
  1404.  
  1405. // Make the row editable when clicked
  1406. // Make the row editable when clicked
  1407. row.onclick = function() {
  1408. keyInput.value = key.split(':')[1]; // If key processing remains as is
  1409. valueInput.value = entryData.value.replace(/^<Event:\s*/, '').slice(0, -1); // Remove <Event: > prefix and surrounding brackets
  1410. probabilityInput.value = entryData.probability;
  1411. timeRangeInput.value = entryData.timeRange;
  1412. currentEditingKey = key; // Set the current key for editing
  1413. };
  1414.  
  1415. entriesList.appendChild(row);
  1416. }
  1417. }
  1418. }
  1419. }
  1420.  
  1421.  
  1422.  
  1423.  
  1424.  
  1425. function highlightSelectedProfile(selectedItem) {
  1426. // Remove highlight from all items
  1427. Array.from(profileList.children).forEach(item => {
  1428. item.style.backgroundColor = '';
  1429. });
  1430. // Highlight the selected item
  1431. selectedItem.style.backgroundColor = '#444444';
  1432. selectedItem.style.borderRadius = '10px';
  1433. selectedItem.style.padding = '10px';
  1434. }
  1435.  
  1436. loadProfiles();
  1437. loadEntries();
  1438.  
  1439. // Append profilePanel to body
  1440. document.body.appendChild(eventsProfilePanel);
  1441. }
  1442.  
  1443. }
  1444.  
  1445. // ==================================================================== RULES ===================================================================
  1446. function RulesScript() {
  1447. // Custom texts to cycle through
  1448. let customRules = [];
  1449. let currentIndex = 0;
  1450.  
  1451. // Function to determine the current rule index by scanning last two messages
  1452. function determineCurrentIndex() {
  1453. const messageItems = document.querySelectorAll('li[class^="messageListItem_"]');
  1454.  
  1455. if (messageItems.length >= 1) {
  1456. // Check the last message first
  1457. const lastMessage = Array.from(messageItems[messageItems.length - 1].querySelectorAll('span')).map(span => span.innerText).join('') || messageItems[messageItems.length - 1].innerText;
  1458.  
  1459. for (let i = 0; i < customRules.length; i++) {
  1460. if (lastMessage.includes(`<Rule${i + 1}:`)) {
  1461. currentIndex = (i + 1) % customRules.length;
  1462. return;
  1463. }
  1464. }
  1465. }
  1466.  
  1467. // If not found in the last message, check the second to last message
  1468. if (messageItems.length >= 2) {
  1469. const secondLastMessage = Array.from(messageItems[messageItems.length - 2].querySelectorAll('span')).map(span => span.innerText).join('') || messageItems[messageItems.length - 2].innerText;
  1470.  
  1471. for (let i = 0; i < customRules.length; i++) {
  1472. if (secondLastMessage.includes(`<Rule${i + 1}:`)) {
  1473. currentIndex = (i + 1) % customRules.length;
  1474. return;
  1475. }
  1476. }
  1477. }
  1478. }
  1479.  
  1480. // Expose necessary elements to be used by the second script
  1481. window.customRuleLogic = {
  1482. customRules,
  1483. determineCurrentIndex,
  1484. getCurrentText: function() {
  1485. determineCurrentIndex();
  1486. const customRule = '\n' + customRules[currentIndex];
  1487. currentIndex = (currentIndex + 1) % customRules.length;
  1488. return customRule;
  1489. }
  1490. };
  1491.  
  1492. // Create Button and Panel UI for Local Storage Key Management
  1493. window.manageRulesButton = document.createElement('div');
  1494. window.manageRulesButton.innerHTML = `
  1495. <button id="toggle-rules-panel"
  1496. style="
  1497. position: relative;
  1498. top: 10px;
  1499. right: 0px;
  1500. left: 10px;
  1501. padding: 7px 15px;
  1502. background: transparent;
  1503. color: #b0b0b0;
  1504. border: none;
  1505. border-radius: 8px;
  1506. font-size: 16px;
  1507. text-align: left;
  1508. cursor: pointer;
  1509. width: 90%;
  1510. display: flex;
  1511. align-items: center;
  1512. gap: 10px;
  1513. transition: background-color 0.1s, color 0.1s;
  1514. z-index: 1001;">
  1515. <svg fill="#B0B0B0" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 492.508 492.508" xml:space="preserve" style="padding-right: 0px; margin-left: 1px;">
  1516. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  1517. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  1518. <g id="SVGRepo_iconCarrier">
  1519. <g>
  1520. <g>
  1521. <path d="M199.493,402.145c0-10.141-8.221-18.361-18.36-18.361H42.475c-10.139,0-18.36,8.221-18.36,18.361 c0,3.195,0.818,6.199,2.255,8.816H0v38.067h223.607v-38.067h-26.369C198.674,408.343,199.493,405.34,199.493,402.145z"></path>
  1522. <path d="M175.898,88.224l117.157,74.396c9.111,4.643,20.43,1.678,26.021-7.129l5.622-8.85c5.938-9.354,3.171-21.75-6.182-27.69 L204.592,46.608c-9.352-5.939-21.748-3.172-27.688,6.182l-5.622,8.851C165.692,70.447,167.82,81.952,175.898,88.224z"></path>
  1523. <path d="M492.456,372.433l-0.082-1.771l-0.146-1.672c-0.075-1.143-0.235-2.159-0.375-3.204c-0.562-4.177-1.521-7.731-2.693-10.946 c-2.377-6.386-5.738-11.222-9.866-14.845c-1.027-0.913-2.126-1.714-3.218-2.528l-3.271-2.443 c-2.172-1.643-4.387-3.218-6.587-4.815c-2.196-1.606-4.419-3.169-6.644-4.729c-2.218-1.571-4.445-3.125-6.691-4.651 c-4.468-3.089-8.983-6.101-13.51-9.103l-6.812-4.464l-6.85-4.405c-4.58-2.911-9.167-5.813-13.785-8.667 c-4.611-2.865-9.24-5.703-13.896-8.496l-13.979-8.363l-14.072-8.22l-14.149-8.096l-14.219-7.987l-14.287-7.882l-14.354-7.773 c-4.802-2.566-9.599-5.137-14.433-7.653c-4.822-2.529-9.641-5.071-14.498-7.548l-4.398,6.928l-22.17-10.449l24.781-39.026 l-117.156-74.395l-60.944,95.974l117.157,74.395l24.781-39.026l18.887,15.622l-4.399,6.929c4.309,3.343,8.657,6.619,12.998,9.91 c4.331,3.305,8.698,6.553,13.062,9.808l13.14,9.686l13.211,9.577l13.275,9.474l13.346,9.361l13.422,9.242l13.514,9.095 c4.51,3.026,9.045,6.009,13.602,8.964c4.547,2.967,9.123,5.882,13.707,8.792l6.898,4.324l6.936,4.266 c4.643,2.818,9.289,5.625,13.985,8.357c2.337,1.383,4.689,2.739,7.055,4.078c2.358,1.349,4.719,2.697,7.106,4 c2.383,1.312,4.75,2.646,7.159,3.912l3.603,1.922c1.201,0.64,2.394,1.296,3.657,1.837c5.036,2.194,10.841,3.18,17.63,2.614 c3.409-0.305,7.034-0.949,11.054-2.216c1.006-0.317,1.992-0.606,3.061-1.023l1.574-0.58l1.639-0.68 c2.185-0.91,4.523-2.063,7.059-3.522C492.513,377.405,492.561,374.799,492.456,372.433z"></path>
  1524. <path d="M67.897,261.877l113.922,72.341c9.354,5.938,21.75,3.172,27.689-6.181l5.621-8.852c5.592-8.808,3.462-20.311-4.615-26.583 L93.358,218.207c-9.111-4.642-20.43-1.678-26.022,7.13l-5.62,8.85C55.775,243.541,58.543,255.938,67.897,261.877z"></path>
  1525. </g>
  1526. </g>
  1527. </g>
  1528. </svg>
  1529. Manage Rules
  1530. </button>
  1531. `;
  1532.  
  1533. const rulesButtonElement = manageRulesButton.querySelector('button');
  1534. rulesButtonElement.onmouseover = () => {
  1535. rulesButtonElement.style.backgroundColor = '#212121';
  1536. rulesButtonElement.style.color = '#ffffff';
  1537. };
  1538. rulesButtonElement.onmouseout = () => {
  1539. rulesButtonElement.style.backgroundColor = 'transparent';
  1540. rulesButtonElement.style.color = '#b0b0b0';
  1541. };
  1542.  
  1543. const rulesPanel = document.createElement('div');
  1544. rulesPanel.style.position = 'fixed';
  1545. rulesPanel.style.top = '50%';
  1546. rulesPanel.style.left = '50%';
  1547. rulesPanel.style.transform = 'translate(-50%, -50%)';
  1548. // Size different for Mobile and Desktop
  1549. if (window.innerWidth <= 768) {
  1550. rulesPanel.style.width = '90%';
  1551. rulesPanel.style.height = '90%';
  1552. } else {
  1553. rulesPanel.style.width = '800px';
  1554. rulesPanel.style.height = '700px';
  1555. }
  1556. rulesPanel.style.backgroundColor = '#2f2f2f';
  1557. rulesPanel.style.padding = '20px';
  1558. rulesPanel.style.overflow = 'hidden'; // Prevent full panel scrolling
  1559. rulesPanel.style.display = 'none';
  1560. rulesPanel.style.zIndex = 1000;
  1561. rulesPanel.style.borderRadius = '10px';
  1562. document.body.appendChild(rulesPanel);
  1563.  
  1564. const title = document.createElement('h2');
  1565. title.innerText = 'Manage Rules';
  1566. title.style.textAlign = 'center';
  1567. title.style.color = '#ffffff';
  1568. title.style.fontSize = '24px';
  1569. title.style.fontWeight = '550';
  1570. title.style.marginBottom = '20px';
  1571. rulesPanel.appendChild(title);
  1572.  
  1573. const closeButton = document.createElement('button');
  1574. closeButton.innerText = '✕';
  1575. closeButton.style.position = 'absolute';
  1576. closeButton.style.borderRadius = '50%';
  1577. closeButton.style.color = '#ffffff';
  1578. closeButton.style.top = '20px';
  1579. closeButton.style.right = '20px';
  1580. closeButton.style.backgroundColor = 'transparent';
  1581. closeButton.style.cursor = 'pointer';
  1582.  
  1583. closeButton.addEventListener('mouseenter', () => {
  1584. closeButton.style.backgroundColor = '#676767';
  1585. });
  1586. closeButton.addEventListener('mouseleave', () => {
  1587. closeButton.style.backgroundColor = 'transparent';
  1588. });
  1589.  
  1590. closeButton.addEventListener('click', () => {
  1591. rulesPanel.style.display = 'none';
  1592. });
  1593. rulesPanel.appendChild(closeButton);
  1594.  
  1595. const rulesListContainer = document.createElement('div');
  1596. rulesListContainer.style.height = 'calc(100% - 110px)'; // Adjust height to leave space for header and button
  1597. rulesListContainer.style.overflowY = 'auto'; // Allow only the rules list to scroll
  1598. rulesPanel.appendChild(rulesListContainer);
  1599.  
  1600. // Create and append the "Add Rule" button once
  1601. const addButton = document.createElement('button');
  1602. addButton.textContent = 'Add Rule';
  1603. addButton.style.margin = '15px auto';
  1604. addButton.style.display = 'block';
  1605. addButton.style.padding = '10px';
  1606. addButton.style.border = 'none';
  1607. addButton.style.width = '90%';
  1608. addButton.style.backgroundColor = '#e0e0e0';
  1609. addButton.style.borderRadius = '20px';
  1610. addButton.style.cursor = 'pointer';
  1611. addButton.addEventListener('click', () => {
  1612. const newRule = `<Rule${customRules.length + 1}: Define your rule here>`;
  1613. customRules.push(newRule);
  1614. updateLocalStorageKeys();
  1615. renderPanel();
  1616. });
  1617. rulesPanel.appendChild(addButton);
  1618.  
  1619. manageRulesButton.addEventListener('click', () => {
  1620. rulesPanel.style.display = rulesPanel.style.display === 'none' ? 'block' : 'none';
  1621. if (rulesPanel.style.display === 'block') {
  1622. renderPanel();
  1623. }
  1624. });
  1625.  
  1626. function renderPanel() {
  1627. rulesListContainer.innerHTML = '';
  1628.  
  1629. customRules.forEach((rule, index) => {
  1630. const ruleContainer = document.createElement('div');
  1631. ruleContainer.style.marginBottom = '15px';
  1632. ruleContainer.style.display = 'flex';
  1633. ruleContainer.style.alignItems = 'center';
  1634. ruleContainer.style.width = '95%';
  1635. ruleContainer.style.gap = '5px'; // Reduced gap to bring elements closer
  1636.  
  1637. const ruleLabel = document.createElement('label');
  1638. ruleLabel.textContent = `Rule ${index + 1}:`;
  1639. ruleLabel.style.color = 'white';
  1640. ruleLabel.style.marginLeft = '5%';
  1641. ruleLabel.style.flex = '0.5'; // Reduced flex to bring the label closer to the input
  1642. ruleContainer.appendChild(ruleLabel);
  1643.  
  1644. // Set the textarea value to the rule text rather than using it as a placeholder
  1645. const ruleInput = document.createElement('textarea');
  1646. ruleInput.value = rule.replace(/<Rule\d+: (.*?)>/, '$1'); // Set the inner text as the value for editing
  1647. ruleInput.style.flex = '2';
  1648. ruleInput.style.padding = '5px'; // Added more padding for easier tapping
  1649. ruleInput.style.borderRadius = '5px';
  1650. ruleInput.style.border = '0.5px solid #444444';
  1651. ruleInput.style.backgroundColor = '#1E1E1E';
  1652. ruleInput.style.color = 'gray';
  1653. ruleInput.style.overflowY = 'hidden'; // Handle vertical overflow automatically
  1654. ruleInput.style.overflowX = 'hidden'; // Prevent horizontal overflow
  1655. ruleInput.style.maxWidth = '100%'; // Prevent textarea from extending beyond container width
  1656. ruleInput.style.boxSizing = 'border-box'; // Ensure padding is included in width calculation
  1657. ruleInput.style.resize = 'none'; // Disable manual resizing to avoid visual clutter
  1658.  
  1659. // Adjust height based on input content
  1660. function adjustHeight(element) {
  1661. element.style.height = 'auto'; // Reset height to calculate the new height
  1662. element.style.height = `${element.scrollHeight}px`; // Set height to match content
  1663. }
  1664.  
  1665. // Adjust height initially to fit content on render
  1666. adjustHeight(ruleInput);
  1667.  
  1668. ruleInput.addEventListener('input', () => {
  1669. adjustHeight(ruleInput);
  1670. ruleInput.style.color = '#D16262'; // Indicate unsaved changes
  1671. });
  1672.  
  1673. ruleContainer.appendChild(ruleInput);
  1674.  
  1675. const updateButton = document.createElement('button');
  1676. updateButton.textContent = 'Update';
  1677. updateButton.style.padding = '5px 10px';
  1678. updateButton.style.border = 'none';
  1679. updateButton.style.backgroundColor = '#1E1E1E';
  1680. updateButton.style.color = 'white';
  1681. updateButton.style.borderRadius = '5px';
  1682. updateButton.style.cursor = 'pointer';
  1683. updateButton.addEventListener('click', () => {
  1684. customRules[index] = `<Rule${index + 1}: ${ruleInput.value}>`;
  1685. updateLocalStorageKeys();
  1686. ruleInput.style.color = 'black'; // Change color to indicate saved state
  1687. });
  1688. ruleContainer.appendChild(updateButton);
  1689.  
  1690. const deleteButton = document.createElement('button');
  1691. deleteButton.textContent = 'Delete';
  1692. deleteButton.style.padding = '5px 10px';
  1693. deleteButton.style.border = 'none';
  1694. deleteButton.style.backgroundColor = '#1E1E1E';
  1695. deleteButton.style.color = 'white';
  1696. deleteButton.style.marginLeft = '5px';
  1697. deleteButton.style.borderRadius = '5px';
  1698. deleteButton.style.cursor = 'pointer';
  1699. deleteButton.addEventListener('click', () => {
  1700. customRules.splice(index, 1);
  1701. updateLocalStorageKeys();
  1702. renderPanel();
  1703. });
  1704. ruleContainer.appendChild(deleteButton);
  1705.  
  1706. // Move rule up/down container
  1707. const moveContainer = document.createElement('div');
  1708. moveContainer.style.display = 'flex';
  1709. moveContainer.style.flexDirection = 'column';
  1710. moveContainer.style.gap = '3px';
  1711.  
  1712. // Move rule up
  1713. if (index > 0) {
  1714. const upButton = document.createElement('button');
  1715. upButton.textContent = '▲';
  1716. upButton.style.padding = '3px';
  1717. upButton.style.border = 'none';
  1718. upButton.style.backgroundColor = 'transparent';
  1719. upButton.style.color = 'white';
  1720. upButton.style.borderRadius = '5px';
  1721. upButton.style.cursor = 'pointer';
  1722. upButton.addEventListener('click', () => {
  1723. [customRules[index - 1], customRules[index]] = [customRules[index], customRules[index - 1]];
  1724. updateLocalStorageKeys();
  1725. renderPanel();
  1726. });
  1727. moveContainer.appendChild(upButton);
  1728. }
  1729.  
  1730. // Move rule down
  1731. if (index < customRules.length - 1) {
  1732. const downButton = document.createElement('button');
  1733. downButton.textContent = '▼';
  1734. downButton.style.padding = '3px';
  1735. downButton.style.border = 'none';
  1736. downButton.style.backgroundColor = 'transparent';
  1737. downButton.style.color = 'white';
  1738. downButton.style.borderRadius = '5px';
  1739. downButton.style.cursor = 'pointer';
  1740. downButton.addEventListener('click', () => {
  1741. [customRules[index], customRules[index + 1]] = [customRules[index + 1], customRules[index]];
  1742. updateLocalStorageKeys();
  1743. renderPanel();
  1744. });
  1745. moveContainer.appendChild(downButton);
  1746. }
  1747.  
  1748. ruleContainer.appendChild(moveContainer);
  1749. rulesListContainer.appendChild(ruleContainer);
  1750. });
  1751.  
  1752. // No longer append the addButton here
  1753.  
  1754. // Adjust height for all textareas after rendering the panel
  1755. adjustAllHeights();
  1756. }
  1757.  
  1758. // Initial adjustment when loading the panel
  1759. function adjustAllHeights() {
  1760. const textareas = rulesListContainer.querySelectorAll('textarea');
  1761. textareas.forEach((textarea) => {
  1762. textarea.style.height = 'auto'; // Reset height to calculate the new height
  1763. textarea.style.height = `${textarea.scrollHeight}px`; // Set height to match content
  1764. });
  1765. }
  1766.  
  1767. function updateLocalStorageKeys() {
  1768. // Clear existing rules in localStorage
  1769. for (let i = 1; i <= localStorage.length; i++) {
  1770. localStorage.removeItem(`Rule${i}`);
  1771. }
  1772. // Set updated rules
  1773. customRules.forEach((rule, index) => {
  1774. localStorage.setItem(`Rule${index + 1}`, rule);
  1775. });
  1776. }
  1777.  
  1778. // Load rules from localStorage
  1779. for (let i = 0; i < localStorage.length; i++) {
  1780. const savedRule = localStorage.getItem(`Rule${i + 1}`);
  1781. if (savedRule) {
  1782. customRules.push(savedRule);
  1783. }
  1784. }
  1785. }
  1786.  
  1787. // ===================================================================== MP3 ====================================================================
  1788. function mp3Script() {
  1789.  
  1790. // Function to get API key from local storage
  1791. function getApiKey() {
  1792. return localStorage.getItem('google_cloud_api_key');
  1793. }
  1794.  
  1795. // Function to set the API key in local storage when "Save API Key" is clicked
  1796. function saveApiKey() {
  1797. const apiKey = document.getElementById('api-key-input').value.trim();
  1798. if (apiKey) {
  1799. localStorage.setItem('google_cloud_api_key', apiKey);
  1800. API_KEY = apiKey;
  1801. alert('API key saved successfully.');
  1802. } else {
  1803. alert('Please enter a valid API key.');
  1804. }
  1805. }
  1806.  
  1807. let API_KEY = getApiKey();
  1808. const API_URL = `https://speech.googleapis.com/v1/speech:recognize?key=${API_KEY}`;
  1809.  
  1810. // Create a button to toggle the transcription panel
  1811. window.mp3ToggleButton = document.createElement('div');
  1812. mp3ToggleButton.innerHTML = `
  1813. <button id="toggle-transcription-panel"
  1814. style="
  1815. display: flex;
  1816. align-items: center;
  1817. gap: 8px;
  1818. position: relative;
  1819. top: 10px;
  1820. right: 0px;
  1821. left: 10px;
  1822. padding: 7px 15px;
  1823. background: transparent;
  1824. color: #b0b0b0;
  1825. border: none;
  1826. border-radius: 8px;
  1827. font-size: 16px;
  1828. text-align: left;
  1829. cursor: pointer;
  1830. width: 90%;
  1831. transition: background-color 0.1s, color 0.1s;
  1832. z-index: 1001;">
  1833. <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" xml:space="preserve" width="20px" height="20px" style="margin-right: -2px;">
  1834. <path d="M6 11L6 13" stroke="#B0B0B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
  1835. <path d="M9 9L9 15" stroke="#B0B0B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
  1836. <path d="M15 9L15 15" stroke="#B0B0B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
  1837. <path d="M18 11L18 13" stroke="#B0B0B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
  1838. <path d="M12 11L12 13" stroke="#B0B0B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
  1839. </svg>
  1840. Show Transcript
  1841. </button>
  1842. `;
  1843.  
  1844. const buttonElement = mp3ToggleButton.querySelector('button');
  1845. buttonElement.onmouseover = () => {
  1846. buttonElement.style.backgroundColor = '#212121';
  1847. buttonElement.style.color = '#ffffff';
  1848. };
  1849. buttonElement.onmouseout = () => {
  1850. buttonElement.style.backgroundColor = 'transparent';
  1851. buttonElement.style.color = '#b0b0b0';
  1852. };
  1853.  
  1854. // Panel HTML
  1855. const panelHTML = `
  1856. <div id="transcription-panel" style="display: none;">
  1857. <h3>MP3 Transcription Tool</h3>
  1858. <input type="text" id="api-key-input" placeholder="Enter Google Cloud API key here" />
  1859. <button id="save-api-key-button">Save API Key</button>
  1860. <input type="text" id="mp3-url" placeholder="Enter MP3 URL here" />
  1861. <button id="transcribe-button">Transcribe</button>
  1862. <textarea id="transcription-result" placeholder="Transcript will appear here..." readonly></textarea>
  1863. </div>
  1864. `;
  1865.  
  1866. document.body.insertAdjacentHTML('beforeend', panelHTML);
  1867.  
  1868. // If API key exists, show it in the input
  1869. if (API_KEY) {
  1870. const apiKeyInput = document.getElementById('api-key-input');
  1871. if (apiKeyInput) {
  1872. apiKeyInput.value = API_KEY;
  1873. }
  1874. }
  1875.  
  1876. GM_addStyle(`
  1877. #transcription-panel {
  1878. position: fixed;
  1879. bottom: 50px;
  1880. right: 10px;
  1881. width: 300px;
  1882. background: #f8f9fa;
  1883. border: 1px solid #ccc;
  1884. padding: 10px;
  1885. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  1886. z-index: 9999;
  1887. font-family: Arial, sans-serif;
  1888. }
  1889. #transcription-panel h3 {
  1890. margin: 0 0 10px;
  1891. font-size: 16px;
  1892. }
  1893. #transcription-panel input, #transcription-panel textarea {
  1894. width: 100%;
  1895. margin-bottom: 10px;
  1896. padding: 5px;
  1897. box-sizing: border-box;
  1898. }
  1899. #transcription-panel button {
  1900. width: 100%;
  1901. padding: 5px;
  1902. cursor: pointer;
  1903. background: #007bff;
  1904. color: white;
  1905. border: none;
  1906. border-radius: 3px;
  1907. margin-bottom: 10px;
  1908. }
  1909. `);
  1910.  
  1911. // Event listener for toggle panel button
  1912. document.addEventListener('click', (event) => {
  1913. const button = event.target.closest('#toggle-transcription-panel');
  1914. if (button) {
  1915. const panel = document.getElementById('transcription-panel');
  1916. if (panel) {
  1917. if (panel.style.display === 'none') {
  1918. panel.style.display = 'block';
  1919. button.innerText = 'Hide MP3 Transcription';
  1920. } else {
  1921. panel.style.display = 'none';
  1922. button.innerText = 'Show Transcript';
  1923. }
  1924. } else {
  1925. console.error('Transcription panel not found!');
  1926. }
  1927. }
  1928. });
  1929.  
  1930. // Event listener for Save API Key button
  1931. document.getElementById('save-api-key-button').addEventListener('click', saveApiKey);
  1932.  
  1933. // Event listener for Transcribe button
  1934. document.getElementById('transcribe-button').addEventListener('click', transcribe);
  1935.  
  1936. function transcribe() {
  1937. const mp3Url = document.getElementById('mp3-url').value.trim();
  1938. if (!mp3Url) {
  1939. alert('Please enter a valid MP3 URL.');
  1940. return;
  1941. }
  1942.  
  1943. // Check if API key is set
  1944. if (!API_KEY) {
  1945. alert('No API key found. Please enter and save your API key first.');
  1946. return;
  1947. }
  1948.  
  1949. // Check if the transcript already exists in local storage
  1950. const storedTranscript = localStorage.getItem(mp3Url);
  1951. if (storedTranscript) {
  1952. document.getElementById('transcription-result').value = storedTranscript;
  1953. return;
  1954. }
  1955.  
  1956. // Fetch MP3 file
  1957. GM_xmlhttpRequest({
  1958. method: 'GET',
  1959. url: mp3Url,
  1960. responseType: 'arraybuffer',
  1961. onload: (response) => {
  1962. const audioBase64 = arrayBufferToBase64(response.response);
  1963. sendToGoogleCloud(audioBase64, mp3Url);
  1964. },
  1965. onerror: (err) => {
  1966. alert('Failed to fetch the MP3 file.');
  1967. console.error(err);
  1968. },
  1969. });
  1970. }
  1971.  
  1972. function sendToGoogleCloud(audioBase64, mp3Url) {
  1973. GM_xmlhttpRequest({
  1974. method: 'POST',
  1975. url: API_URL,
  1976. headers: { 'Content-Type': 'application/json' },
  1977. data: JSON.stringify({
  1978. config: {
  1979. encoding: 'MP3',
  1980. sampleRateHertz: 16000,
  1981. languageCode: 'en-US',
  1982. },
  1983. audio: {
  1984. content: audioBase64,
  1985. },
  1986. }),
  1987. onload: (response) => {
  1988. const result = JSON.parse(response.responseText);
  1989. if (result.error) {
  1990. alert(`Error: ${result.error.message}`);
  1991. } else {
  1992. const transcript = result.results
  1993. ?.map((r) => r.alternatives[0].transcript)
  1994. .join('\n') || 'No transcript found.';
  1995. document.getElementById('transcription-result').value = transcript;
  1996. localStorage.setItem(mp3Url, transcript);
  1997. manageLocalStorageLimit();
  1998. showTranscriptionDoneMessage();
  1999. }
  2000. },
  2001. onerror: (err) => {
  2002. alert('Failed to process the transcription.');
  2003. console.error(err);
  2004. },
  2005. });
  2006. }
  2007.  
  2008. function arrayBufferToBase64(buffer) {
  2009. const bytes = new Uint8Array(buffer);
  2010. let binary = '';
  2011. for (let i = 0; i < bytes.byteLength; i++) {
  2012. binary += String.fromCharCode(bytes[i]);
  2013. }
  2014. return btoa(binary);
  2015. }
  2016.  
  2017. const observer = new MutationObserver(() => {
  2018. const messageItems = document.querySelectorAll('div[class*="messageContent_"]');
  2019. const lastMessage = messageItems[messageItems.length - 1];
  2020.  
  2021. if (lastMessage) {
  2022. const mp3LinkMatch = lastMessage.innerText.match(/https:\/\/files\.shapes\.inc\/.*\.mp3/);
  2023. if (mp3LinkMatch) {
  2024. const mp3Link = mp3LinkMatch[0];
  2025. const storedLink = localStorage.getItem('lastMp3Link');
  2026.  
  2027. // If no API key, do not attempt automatic transcription
  2028. if (!API_KEY) {
  2029. return;
  2030. }
  2031.  
  2032. // Check if the link is new or different
  2033. if (mp3Link !== storedLink) {
  2034. localStorage.setItem('lastMp3Link', mp3Link);
  2035. manageLocalStorageLimit();
  2036. document.getElementById('mp3-url').value = mp3Link;
  2037. transcribe();
  2038. }
  2039. }
  2040. }
  2041. });
  2042.  
  2043. observer.observe(document.body, { childList: true, subtree: true });
  2044.  
  2045. function manageLocalStorageLimit() {
  2046. const mp3KeysPrefix = 'mp3Link_';
  2047. const mp3Keys = Object.keys(localStorage).filter((key) => key.startsWith(mp3KeysPrefix));
  2048.  
  2049. // Store the new mp3 link with a timestamp
  2050. const timestamp = Date.now();
  2051. const lastLink = localStorage.getItem('lastMp3Link');
  2052. if (lastLink) {
  2053. localStorage.setItem(`${mp3KeysPrefix}${timestamp}`, lastLink);
  2054. }
  2055.  
  2056. // Limit to 10 entries
  2057. if (mp3Keys.length >= 10) {
  2058. mp3Keys.sort((a, b) => {
  2059. const timeA = parseInt(a.replace(mp3KeysPrefix, ''), 10);
  2060. const timeB = parseInt(b.replace(mp3KeysPrefix, ''), 10);
  2061. return timeA - timeB;
  2062. });
  2063. while (mp3Keys.length >= 10) {
  2064. localStorage.removeItem(mp3Keys.shift());
  2065. }
  2066. }
  2067. }
  2068.  
  2069. function showTranscriptionDoneMessage() {
  2070. let messageDiv = document.getElementById('transcription-done-message');
  2071. if (!messageDiv) {
  2072. messageDiv = document.createElement('div');
  2073. messageDiv.id = 'transcription-done-message';
  2074. messageDiv.innerText = 'Transcript done!';
  2075. document.body.appendChild(messageDiv);
  2076. }
  2077.  
  2078. messageDiv.style.display = 'block';
  2079. setTimeout(() => {
  2080. messageDiv.style.display = 'none';
  2081. }, 3000);
  2082. }
  2083. }
  2084.  
  2085. // =================================================================== STYLES ===================================================================
  2086. function StylesScript() {
  2087.  
  2088. // Add custom CSS rule to modify the channel text area
  2089. const style = document.createElement('style');
  2090. style.innerHTML = `
  2091. .channelTextArea_bdf0de {
  2092. position: relative;
  2093. width: 120%;
  2094. text-indent: 0;
  2095. border-radius: 8px;
  2096. }
  2097. .themedBackground_bdf0de {
  2098. background: #2F2F2F;
  2099. }
  2100. .chatContent_a7d72e {
  2101. position: relative;
  2102. display: flex;
  2103. flex-direction: column;
  2104. min-width: 0;
  2105. min-height: 0;
  2106. flex: 1 1 auto;
  2107. background: #212121 !important;
  2108. }
  2109.  
  2110. .theme-dark .container_ee69e0 {
  2111. background: #191919;
  2112. }
  2113. .theme-dark .themed_fc4f04 {
  2114. background: #212121;
  2115. }
  2116.  
  2117. .footer_f8ec41 {
  2118. background: #131313;
  2119. }
  2120. .theme-dark .container_b2ca13 {
  2121. background: #191919;
  2122. }
  2123.  
  2124. .wrapper_fea3ef {
  2125. background-color: #131313;
  2126. display: none !important;
  2127.  
  2128. }
  2129.  
  2130.  
  2131.  
  2132. `;
  2133. document.head.appendChild(style);
  2134.  
  2135. // Function to hide the targeted elements
  2136. function hideElements() {
  2137. // Select all elements that match the provided pattern
  2138. const elements = document.querySelectorAll("[id^='message-accessories-'] > article > div > div > div.embedProvider_b0068a.embedMargin_b0068a, [id^='message-accessories-'] > article > div > div > div.embedTitle_b0068a.embedMargin_b0068a, .buttons_bdf0de .expression-picker-chat-input-button.buttonContainer_bdf0de, .channelAppLauncher_df39bd .buttonContainer_df39bd.app-launcher-entrypoint");
  2139.  
  2140. // Iterate over each element and hide it
  2141. elements.forEach(element => {
  2142. element.style.display = 'none';
  2143. });
  2144. }
  2145.  
  2146. // Run the function initially to hide elements present at page load
  2147. hideElements();
  2148.  
  2149. // Observe mutations to the DOM and hide elements when new ones are added
  2150. const observer = new MutationObserver(hideElements);
  2151. observer.observe(document.body, { childList: true, subtree: true });
  2152.  
  2153. // Create the toggleButton div
  2154. // Create the toggleButton div
  2155. window.serverbartoggleButton = document.createElement('div');
  2156. serverbartoggleButton.innerHTML = `
  2157. <button id="toggle-server-panel"
  2158. style="
  2159. display: flex;
  2160. align-items: center;
  2161. gap: 8px; /* Add spacing between icon and text */
  2162. position: relative;
  2163. top: 10px;
  2164. right: 0px;
  2165. left: 10px;
  2166. padding: 7px 15px;
  2167. background: transparent;
  2168. color: #b0b0b0;
  2169. border: none;
  2170. border-radius: 8px;
  2171. font-size: 16px;
  2172. text-align: left;
  2173. cursor: pointer;
  2174. width: 90%;
  2175. transition: background-color 0.1s, color 0.1s;
  2176. z-index: 1001;">
  2177. <svg width="16px" height="16px" viewBox="0 0 16 16" style="padding-right: 0px; margin-left: 2px" xmlns="http://www.w3.org/2000/svg" fill="#000000">
  2178. <g fill="#B0B0B0">
  2179. <path d="m 6.5 14 v -12 h -5 v 12 z m 0 0" fill-opacity="0.34902"></path>
  2180. <path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -8 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 10 c 0.570312 0 1 0.429688 1 1 v 8 c 0 0.570312 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"></path>
  2181. <path d="m 6 2 h 1 v 12 h -1 z m 0 0"></path>
  2182. </g>
  2183. </svg>
  2184. Toggle Sidebar
  2185. </button>
  2186. `;
  2187.  
  2188. // Append the toggleButton to the desired parent element (e.g., `DCstoragePanel`)
  2189. //window.DCstoragePanel.appendChild(serverbartoggleButton);
  2190.  
  2191. // Function to toggle .itemsContainer_fea3ef visibility
  2192. function servertoggleItemsContainer() {
  2193. const serveritemsContainer = document.querySelectorAll('.wrapper_fea3ef');
  2194. serveritemsContainer.forEach(container => {
  2195. const currentDisplay = window.getComputedStyle(container).getPropertyValue('display');
  2196.  
  2197. if (currentDisplay === 'none') {
  2198. container.setAttribute('style', 'display: flex !important;');
  2199. } else {
  2200. container.setAttribute('style', 'display: none !important;');
  2201. }
  2202. });
  2203. }
  2204.  
  2205.  
  2206. // Add event listener to the toggle button
  2207. serverbartoggleButton.addEventListener('click', servertoggleItemsContainer);
  2208.  
  2209. const serverbuttonElement = serverbartoggleButton.querySelector('button');
  2210. serverbuttonElement.onmouseover = () => {
  2211. serverbuttonElement.style.backgroundColor = '#212121';
  2212. serverbuttonElement.style.color = '#ffffff';
  2213. };
  2214. serverbuttonElement.onmouseout = () => {
  2215. serverbuttonElement.style.backgroundColor = 'transparent';
  2216. serverbuttonElement.style.color = '#b0b0b0';
  2217. };
  2218.  
  2219.  
  2220. }
  2221.  
  2222. // ================================================================== < > HIDER =================================================================
  2223. function HiderScript() {
  2224. function hideEnclosedEntries() {
  2225. const messageItems = document.querySelectorAll('li[class^="messageListItem_"]');
  2226.  
  2227. messageItems.forEach(messageItem => {
  2228. const spans = messageItem.querySelectorAll('div[class*="messageContent_"] span');
  2229.  
  2230. let isHiding = false;
  2231. spans.forEach(span => {
  2232. const text = span.textContent.trim();
  2233.  
  2234. // Start hiding when encountering '<'
  2235. if (text.startsWith('<') && !isHiding) {
  2236. isHiding = true;
  2237. }
  2238.  
  2239. // Apply hiding style if within an enclosed entry
  2240. if (isHiding) {
  2241. span.style.opacity = '0'; // Make it invisible
  2242. span.style.position = 'absolute'; // Remove it from the document flow
  2243. }
  2244.  
  2245. // Stop hiding when encountering '>'
  2246. if (text.endsWith('>') && isHiding) {
  2247. isHiding = false;
  2248. }
  2249. });
  2250. });
  2251. }
  2252.  
  2253. // Observe for new messages being added to the DOM
  2254. const observer = new MutationObserver(mutations => {
  2255. mutations.forEach(() => {
  2256. hideEnclosedEntries();
  2257. });
  2258. });
  2259.  
  2260. // Start observing the entire document body for changes
  2261. observer.observe(document.body, { childList: true, subtree: true });
  2262.  
  2263. // Initial run
  2264. hideEnclosedEntries();
  2265.  
  2266. }
  2267.  
  2268. // ================================================================== NOTIFIER ==================================================================
  2269. function NotifierScript() {
  2270.  
  2271. // Utility: Generate a unique ID for entries
  2272. function generateId() {
  2273. return 'id-' + Math.random().toString(36).substr(2, 9);
  2274. }
  2275.  
  2276. // The storage scheme requested:
  2277. // Profiles: Key: "notifier.profile:ProfileName" Value: "{}" (just an empty object for now)
  2278. // Selected profile: Key: "selectedProfile.notifier" Value: "ProfileName"
  2279. // Entries: Key: "ProfileName.notifiers" Value: JSON array of entries
  2280.  
  2281. // Styles
  2282. GM_addStyle(`
  2283. #calendarToggleBtn {
  2284. position: fixed;
  2285. top: 10px;
  2286. left: 10px;
  2287. z-index: 9999;
  2288. padding: 5px 10px;
  2289. background: #444;
  2290. color: #eee;
  2291. font-size: 14px;
  2292. border: none;
  2293. cursor: pointer;
  2294. }
  2295.  
  2296. #calendarPanel {
  2297. position: fixed;
  2298. top: 50%;
  2299. left: 50%;
  2300. transform: translate(-50%, -50%);
  2301. width: 70%;
  2302. height: 80%;
  2303. background: #222;
  2304. z-index: 9999;
  2305. border: 1px solid #555;
  2306. display: none;
  2307. box-sizing: border-box;
  2308. overflow: auto; /* Enable scrollbars if needed */
  2309. font-family: Arial, sans-serif;
  2310. font-size: 14px;
  2311. color: #eee;
  2312. resize: both; /* Allow resizing from bottom and right edges */
  2313. min-width: 300px; /* Optional: set a minimum width so it's not too small */
  2314. min-height: 200px; /* Optional: set a minimum height as well */
  2315. }
  2316.  
  2317. #calendarPanel::after {
  2318. content: "";
  2319. position: absolute;
  2320. right: 0;
  2321. bottom: 0;
  2322. width: 15px;
  2323. height: 15px;
  2324. background: #444;
  2325. border-top: 1px solid #555;
  2326. border-left: 1px solid #555;
  2327. cursor: se-resize;
  2328. }
  2329.  
  2330.  
  2331. #calendarPanelHeader {
  2332. display: flex;
  2333. align-items: center;
  2334. justify-content: space-between;
  2335. padding: 5px;
  2336. background: #333;
  2337. border-bottom: 1px solid #555;
  2338. }
  2339.  
  2340. #calendarPanelClose {
  2341. cursor: pointer;
  2342. font-weight: bold;
  2343. color: #eee;
  2344. }
  2345.  
  2346. #calendarPanelContent {
  2347. display: flex;
  2348. width: 100%;
  2349. height: calc(100% - 30px);
  2350. }
  2351.  
  2352. #profileSection {
  2353. width: 15%; /* Reduced width */
  2354. border-right: 1px solid #555;
  2355. overflow-y: auto;
  2356. padding: 10px;
  2357. box-sizing: border-box;
  2358. background: #222;
  2359. color: #eee;
  2360. }
  2361.  
  2362. #profileList {
  2363. margin: 0;
  2364. padding: 0;
  2365. list-style: none;
  2366. }
  2367.  
  2368. #profileList li {
  2369. cursor: pointer;
  2370. padding: 5px;
  2371. margin-bottom: 5px;
  2372. background: #333;
  2373. border: 1px solid #444;
  2374. color: #eee;
  2375. display: flex;
  2376. justify-content: space-between;
  2377. align-items: center;
  2378. }
  2379.  
  2380. #profileList li span.profileName {
  2381. flex: 1;
  2382. }
  2383.  
  2384. #profileList li button.deleteProfileBtn {
  2385. background: #600;
  2386. border: 1px solid #800;
  2387. color: #eee;
  2388. margin-left: 5px;
  2389. cursor: pointer;
  2390. font-size: 12px;
  2391. padding: 2px 5px;
  2392. }
  2393.  
  2394. #profileList li.selected {
  2395. background: #444;
  2396. }
  2397.  
  2398. #addProfileForm {
  2399. display: flex;
  2400. margin-top: 10px;
  2401. }
  2402.  
  2403. #addProfileForm input[type=text] {
  2404. flex: 1;
  2405. padding: 3px;
  2406. background: #333;
  2407. border: 1px solid #444;
  2408. color: #eee;
  2409. }
  2410.  
  2411. #addProfileForm button {
  2412. padding: 3px;
  2413. background: #444;
  2414. border: 1px solid #555;
  2415. color: #eee;
  2416. cursor: pointer;
  2417. }
  2418.  
  2419. #calendarSection {
  2420. width: 85%; /* Increased width */
  2421. padding: 10px;
  2422. box-sizing: border-box;
  2423. overflow: auto;
  2424. background: #222;
  2425. color: #eee;
  2426. }
  2427.  
  2428. #calendarNav {
  2429. display: flex;
  2430. justify-content: space-between;
  2431. align-items: center;
  2432. margin-bottom: 10px;
  2433. }
  2434.  
  2435. #calendarNav button {
  2436. padding: 3px 5px;
  2437. background: #444;
  2438. color: #eee;
  2439. border: 1px solid #555;
  2440. cursor: pointer;
  2441. }
  2442.  
  2443.  
  2444. #calendarGrid {
  2445. display: grid;
  2446. grid-template-columns: repeat(7, 1fr);
  2447. grid-gap: 10px; /* Add a bit more spacing between days */
  2448. }
  2449.  
  2450. .calendarDay {
  2451. background: #333;
  2452. border: 1px solid #444;
  2453. height: 150px;
  2454. width: 100%;
  2455. position: relative;
  2456. padding: 5px;
  2457. box-sizing: border-box;
  2458. overflow: hidden;
  2459. font-size: 12px;
  2460. color: #eee;
  2461. }
  2462.  
  2463. .calendarDayEntries {
  2464. display: block;
  2465. overflow-y: auto;
  2466. max-height: 120px; /* Adjust as needed */
  2467. text-overflow: ellipsis;
  2468. white-space: normal;
  2469. word-wrap: break-word;
  2470. font-size: 11px;
  2471. color: #eee;
  2472. outline: none;
  2473. }
  2474.  
  2475. .calendarDayEntries:focus {
  2476. outline: 2px solid #888; /* Visible focus indicator */
  2477. }
  2478.  
  2479.  
  2480. /* Truncate each individual entry to 3 lines */
  2481. .calendarEntry {
  2482. display: -webkit-box;
  2483. -webkit-line-clamp: 3; /* Number of lines to display */
  2484. -webkit-box-orient: vertical;
  2485. overflow: hidden;
  2486. text-overflow: ellipsis;
  2487. line-height: 1.2em;
  2488. max-height: 3.6em; /* line-height * lines to display */
  2489. margin: 2px 0;
  2490. padding: 2px;
  2491. background: #444;
  2492. border: 1px solid #555;
  2493. color: #eee;
  2494. cursor: move;
  2495. }
  2496.  
  2497. .calendarEntry:hover {
  2498. background: #555;
  2499. }
  2500.  
  2501. /* Modal Styles */
  2502. #entryModal {
  2503. position: fixed;
  2504. top: 0;
  2505. left: 0;
  2506. width: 100vw;
  2507. height: 100vh;
  2508. background: rgba(0,0,0,0.5);
  2509. z-index: 10000;
  2510. display: none;
  2511. justify-content: center;
  2512. align-items: center;
  2513. }
  2514.  
  2515. #entryModalContent {
  2516. background: #333;
  2517. padding: 20px;
  2518. border: 1px solid #444;
  2519. width: 400px;
  2520. box-sizing: border-box;
  2521. color: #eee;
  2522. }
  2523.  
  2524. #entryModalContent h3 {
  2525. margin-top: 0;
  2526. color: #eee;
  2527. }
  2528.  
  2529. #entryModalContent label {
  2530. display: block;
  2531. margin-bottom: 5px;
  2532. font-weight: bold;
  2533. color: #eee;
  2534. }
  2535.  
  2536. #entryModalContent input[type=text],
  2537. #entryModalContent select,
  2538. #entryModalContent input[type=time],
  2539. #entryModalContent input[type=number],
  2540. #entryModalContent input[type=range],
  2541. #entryModalContent input[type=date] {
  2542. width: 100%;
  2543. box-sizing: border-box;
  2544. padding: 5px;
  2545. margin-bottom: 10px;
  2546. background: #444;
  2547. border: 1px solid #555;
  2548. color: #eee;
  2549. }
  2550.  
  2551. #entryModalContent .timeframe {
  2552. display: flex;
  2553. justify-content: space-between;
  2554. gap: 5px;
  2555. }
  2556.  
  2557. #entryModalContent .timeframe input[type=time] {
  2558. width: 48%;
  2559. }
  2560.  
  2561. #entryModalContent .recurrenceOptions {
  2562. margin-bottom: 10px;
  2563. }
  2564.  
  2565. #entryModalContent .weeklyDays {
  2566. display: flex;
  2567. flex-wrap: wrap;
  2568. gap: 5px;
  2569. }
  2570.  
  2571. #entryModalContent .weeklyDays label {
  2572. display: flex;
  2573. align-items: center;
  2574. font-weight: normal;
  2575. color: #eee;
  2576. }
  2577.  
  2578. #entryModalButtons {
  2579. display: flex;
  2580. justify-content: space-between;
  2581. gap: 10px;
  2582. margin-top: 10px;
  2583. }
  2584.  
  2585. #entryModalButtonsLeft {
  2586. display: flex;
  2587. gap: 10px;
  2588. }
  2589.  
  2590. #entryModalButtons button {
  2591. padding: 5px 10px;
  2592. background: #444;
  2593. border: 1px solid #555;
  2594. color: #eee;
  2595. cursor: pointer;
  2596. }
  2597.  
  2598. #probabilityWrapper {
  2599. margin-bottom: 10px;
  2600. }
  2601. #probabilityValue {
  2602. text-align: center;
  2603. margin-top: -5px;
  2604. margin-bottom: 10px;
  2605. color: #eee;
  2606. }
  2607. .currentDay {
  2608. border: 2px solid #98cfb2; /* Gold border */
  2609. box-shadow: 0 0 10px #98cfb2; /* Gold glow */
  2610. background: #444; /* Slight background change to distinguish */
  2611. }
  2612.  
  2613. /* Optional: Add a hover effect for the current day */
  2614. .currentDay:hover {
  2615. background: #555;
  2616. }
  2617. `);
  2618.  
  2619.  
  2620. // Create the toggle button container
  2621. window.notifierToggleButton = document.createElement('div');
  2622. notifierToggleButton.innerHTML = `
  2623. <button id="toggle-notifier-panel"
  2624. style="
  2625. display: flex;
  2626. align-items: center;
  2627. gap: 8px;
  2628. position: relative;
  2629. top: 10px;
  2630. right: 0px;
  2631. left: 10px;
  2632. padding: 7px 15px;
  2633. background: transparent;
  2634. color: #b0b0b0;
  2635. border: none;
  2636. border-radius: 8px;
  2637. font-size: 16px;
  2638. text-align: left;
  2639. cursor: pointer;
  2640. width: 90%;
  2641. transition: background-color 0.1s, color 0.1s;
  2642. z-index: 1001;">
  2643. <svg width="17px" height="17px" viewBox="0 0 24 24"
  2644. fill="#b0b0b0" stroke="#b0b0b0"
  2645. xmlns="http://www.w3.org/2000/svg">
  2646. <path d="M8,21h8c2.8,0,5-2.2,5-5v-4c0-0.6-0.4-1-1-1s-1,0.4-1,1v4c0,1.7-1.3,3-3,3H8
  2647. c-1.7,0-3-1.3-3-3V8c0-1.7,1.3-3,3-3h4c0.6,0,1-0.4,1-1s-0.4-1-1-1H8C5.2,3,3,5.2,3,8v8C3,18.8,5.2,21,8,21z"></path>
  2648. <path d="M18,2c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S20.2,2,18,2z
  2649. M18,8c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S19.1,8,18,8z"></path>
  2650. </svg>
  2651. Show Notifiers
  2652. </button>
  2653. `;
  2654.  
  2655. // Apply hover effects to the button
  2656. const notifierButtonElement = notifierToggleButton.querySelector('button');
  2657. notifierButtonElement.onmouseover = () => {
  2658. notifierButtonElement.style.backgroundColor = '#212121';
  2659. notifierButtonElement.style.color = '#ffffff';
  2660. };
  2661. notifierButtonElement.onmouseout = () => {
  2662. notifierButtonElement.style.backgroundColor = 'transparent';
  2663. notifierButtonElement.style.color = '#b0b0b0';
  2664. };
  2665.  
  2666. // Later, append to your panel as needed:
  2667. // DCstoragePanel.appendChild(notifierToggleButton);
  2668.  
  2669.  
  2670. // Create panel
  2671. let panel = document.createElement('div');
  2672. panel.id = 'calendarPanel';
  2673. panel.innerHTML = `
  2674. <div id="calendarPanelHeader">
  2675. <span>Profiles & Calendar</span>
  2676. <span id="calendarPanelClose" title="Close">X</span>
  2677. </div>
  2678. <div id="calendarPanelContent">
  2679. <div id="profileSection">
  2680. <ul id="profileList"></ul>
  2681. <form id="addProfileForm">
  2682. <input type="text" id="newProfileInput" placeholder="New profile name" />
  2683. <button type="submit">Add</button>
  2684. </form>
  2685. </div>
  2686. <div id="calendarSection">
  2687. <div id="calendarNav">
  2688. <button id="prevMonthBtn">&lt;</button>
  2689. <span id="currentMonthLabel"></span>
  2690. <button id="nextMonthBtn">&gt;</button>
  2691. </div>
  2692. <div id="dayNamesRow" style="
  2693. display: grid;
  2694. grid-template-columns: repeat(7, 1fr);
  2695. text-align: center;
  2696. font-weight: bold;
  2697. border-bottom: 1px solid #444;
  2698. margin-bottom: 5px;
  2699. padding-bottom: 5px;
  2700. ">
  2701. <div>Mon</div>
  2702. <div>Tue</div>
  2703. <div>Wed</div>
  2704. <div>Thu</div>
  2705. <div>Fri</div>
  2706. <div>Sat</div>
  2707. <div>Sun</div>
  2708. </div>
  2709.  
  2710. <div id="calendarGrid"></div>
  2711.  
  2712. <div id="calendarGrid"></div>
  2713. </div>
  2714. </div>
  2715. `;
  2716. document.body.appendChild(panel);
  2717.  
  2718. // Create entry modal
  2719. let entryModal = document.createElement('div');
  2720. entryModal.id = 'entryModal';
  2721. entryModal.innerHTML = `
  2722. <div id="entryModalContent">
  2723. <h3>Add/Edit Entry</h3>
  2724. <label for="entryTitle">Title:</label>
  2725. <input type="text" id="entryTitle" placeholder="Entry title">
  2726.  
  2727. <label>Start Date:</label>
  2728. <input type="date" id="entryStartDate">
  2729.  
  2730. <label>End Date:</label>
  2731. <input type="date" id="entryEndDate">
  2732.  
  2733. <label>Time / Timeframe:</label>
  2734. <div class="timeframe">
  2735. <input type="time" id="entryStartTime">
  2736. <input type="time" id="entryEndTime">
  2737. </div>
  2738.  
  2739. <label for="recurrenceType">Recurrence:</label>
  2740. <select id="recurrenceType">
  2741. <option value="none">None</option>
  2742. <option value="daily">Daily</option>
  2743. <option value="weekly">Weekly (Select days)</option>
  2744. <option value="monthly">Monthly (Same day of month)</option>
  2745. </select>
  2746.  
  2747. <div class="recurrenceOptions">
  2748. <div id="weeklyDaysOptions" style="display:none;">
  2749. <div class="weeklyDays">
  2750. <label><input type="checkbox" value="1">Mon</label>
  2751. <label><input type="checkbox" value="2">Tue</label>
  2752. <label><input type="checkbox" value="3">Wed</label>
  2753. <label><input type="checkbox" value="4">Thu</label>
  2754. <label><input type="checkbox" value="5">Fri</label>
  2755. <label><input type="checkbox" value="6">Sat</label>
  2756. <label><input type="checkbox" value="0">Sun</label>
  2757. </div>
  2758. </div>
  2759. </div>
  2760.  
  2761.  
  2762. <div id="dailyDaysOptions" style="display:none;">
  2763. <div class="weeklyDays">
  2764. <label><input type="checkbox" value="1" checked>Mon</label>
  2765. <label><input type="checkbox" value="2" checked>Tue</label>
  2766. <label><input type="checkbox" value="3" checked>Wed</label>
  2767. <label><input type="checkbox" value="4" checked>Thu</label>
  2768. <label><input type="checkbox" value="5" checked>Fri</label>
  2769. <label><input type="checkbox" value="6" checked>Sat</label>
  2770. <label><input type="checkbox" value="0" checked>Sun</label>
  2771. </div>
  2772. <p style="font-size:12px;color:#eee;">(Uncheck any days you do not want to include in the daily schedule)</p>
  2773. </div>
  2774.  
  2775.  
  2776.  
  2777. <div id="probabilityWrapper">
  2778. <label for="entryProbability">Probability (%):</label>
  2779. <input type="range" id="entryProbability" min="0" max="100" value="100">
  2780. <div id="probabilityValue">100%</div>
  2781. </div>
  2782.  
  2783. <div id="entryModalButtons">
  2784. <div id="entryModalButtonsLeft">
  2785. <button id="saveEntryBtn">Save</button>
  2786. <button id="cancelEntryBtn">Cancel</button>
  2787. </div>
  2788. <button id="deleteEntryBtn" style="display:none;background:#600;border:1px solid #800;">Delete</button>
  2789. </div>
  2790. </div>
  2791. `;
  2792. document.body.appendChild(entryModal);
  2793.  
  2794. let currentProfile = null;
  2795. // Load selected profile
  2796. let lastProfile = localStorage.getItem('selectedProfile.notifier');
  2797. if (lastProfile && localStorage.getItem('notifier.profile:' + lastProfile)) {
  2798. currentProfile = lastProfile;
  2799. }
  2800.  
  2801. let currentMonth = new Date().getMonth();
  2802. let currentYear = new Date().getFullYear();
  2803. let editingEventId = null; // if we're editing an event
  2804.  
  2805. // Elements
  2806. const calendarGrid = panel.querySelector('#calendarGrid');
  2807. const currentMonthLabel = panel.querySelector('#currentMonthLabel');
  2808. const profileListEl = panel.querySelector('#profileList');
  2809. const addProfileForm = panel.querySelector('#addProfileForm');
  2810. const newProfileInput = panel.querySelector('#newProfileInput');
  2811.  
  2812. const modal = document.getElementById('entryModal');
  2813. const entryTitle = document.getElementById('entryTitle');
  2814. const entryStartTime = document.getElementById('entryStartTime');
  2815. const entryEndTime = document.getElementById('entryEndTime');
  2816. const recurrenceType = document.getElementById('recurrenceType');
  2817. const weeklyDaysOptions = document.getElementById('weeklyDaysOptions');
  2818. const dailyDaysOptions = document.getElementById('dailyDaysOptions');
  2819. const saveEntryBtn = document.getElementById('saveEntryBtn');
  2820. const cancelEntryBtn = document.getElementById('cancelEntryBtn');
  2821. const deleteEntryBtn = document.getElementById('deleteEntryBtn');
  2822. const entryProbability = document.getElementById('entryProbability');
  2823. const probabilityValue = document.getElementById('probabilityValue');
  2824. const entryStartDate = document.getElementById('entryStartDate');
  2825. const entryEndDate = document.getElementById('entryEndDate');
  2826.  
  2827. // After the calendar is rendered:
  2828. document.querySelectorAll('.calendarDayEntries').forEach(dayEntries => {
  2829. // Make it focusable
  2830. dayEntries.setAttribute('tabindex', '0');
  2831.  
  2832. // On hover, focus the entry panel so keyboard scroll will target it
  2833. dayEntries.addEventListener('mouseenter', () => {
  2834. dayEntries.focus();
  2835. });
  2836.  
  2837. // Optional: Remove focus on mouse leave (if desired)
  2838. dayEntries.addEventListener('mouseleave', () => {
  2839. dayEntries.blur();
  2840. });
  2841. });
  2842.  
  2843.  
  2844.  
  2845. notifierButtonElement.addEventListener('click', () => {
  2846. panel.style.display = (panel.style.display === 'none' || panel.style.display === '') ? 'block' : 'none';
  2847. renderProfiles();
  2848. renderCalendar();
  2849. });
  2850.  
  2851. document.getElementById('calendarPanelClose').addEventListener('click', () => {
  2852. panel.style.display = 'none';
  2853. });
  2854.  
  2855. addProfileForm.addEventListener('submit', (e) => {
  2856. e.preventDefault();
  2857. let name = newProfileInput.value.trim();
  2858. if (name !== '') {
  2859. createProfile(name);
  2860. currentProfile = name;
  2861. saveSelectedProfile();
  2862. newProfileInput.value = '';
  2863. renderProfiles();
  2864. renderCalendar();
  2865. }
  2866. });
  2867.  
  2868. document.getElementById('prevMonthBtn').addEventListener('click', () => {
  2869. currentMonth--;
  2870. if (currentMonth < 0) {
  2871. currentMonth = 11;
  2872. currentYear--;
  2873. }
  2874. renderCalendar();
  2875. });
  2876.  
  2877. document.getElementById('nextMonthBtn').addEventListener('click', () => {
  2878. currentMonth++;
  2879. if (currentMonth > 11) {
  2880. currentMonth = 0;
  2881. currentYear++;
  2882. }
  2883. renderCalendar();
  2884. });
  2885.  
  2886. recurrenceType.addEventListener('change', () => {
  2887. if (recurrenceType.value === 'weekly') {
  2888. weeklyDaysOptions.style.display = 'block';
  2889. dailyDaysOptions.style.display = 'none';
  2890. } else if (recurrenceType.value === 'daily') {
  2891. dailyDaysOptions.style.display = 'block';
  2892. weeklyDaysOptions.style.display = 'none';
  2893. } else {
  2894. weeklyDaysOptions.style.display = 'none';
  2895. dailyDaysOptions.style.display = 'none';
  2896. }
  2897. });
  2898.  
  2899.  
  2900. saveEntryBtn.addEventListener('click', () => {
  2901. saveEntry();
  2902. });
  2903.  
  2904. cancelEntryBtn.addEventListener('click', () => {
  2905. closeModal();
  2906. });
  2907.  
  2908. deleteEntryBtn.addEventListener('click', () => {
  2909. if (editingEventId) {
  2910. deleteEntry(editingEventId);
  2911. }
  2912. });
  2913.  
  2914. entryProbability.addEventListener('input', () => {
  2915. probabilityValue.textContent = entryProbability.value + '%';
  2916. });
  2917.  
  2918. // Drag & Drop handlers
  2919. calendarGrid.addEventListener('dragover', (e) => {
  2920. e.preventDefault();
  2921. });
  2922.  
  2923. calendarGrid.addEventListener('drop', (e) => {
  2924. e.preventDefault();
  2925. let eventId = e.dataTransfer.getData('text');
  2926. let target = e.target.closest('.calendarDay');
  2927. if (!target || !eventId) return;
  2928. let dayNumber = parseInt(target.querySelector('.dayNumber')?.textContent, 10);
  2929. if (!dayNumber) return;
  2930.  
  2931. let entries = getAllEntries();
  2932. let entry = entries.find(en => en.id === eventId);
  2933. if (!entry) return;
  2934.  
  2935. let dropDate = new Date(currentYear, currentMonth, dayNumber);
  2936. adjustRecurrenceForDrop(entry, dropDate);
  2937. setAllEntries(entries);
  2938. renderCalendar();
  2939. });
  2940.  
  2941. function createProfile(profileName) {
  2942. if (!localStorage.getItem('notifier.profile:' + profileName)) {
  2943. localStorage.setItem('notifier.profile:' + profileName, '{}');
  2944. // Also initialize entries
  2945. localStorage.setItem(profileName + '.notifiers', '[]');
  2946. }
  2947. }
  2948.  
  2949. function deleteProfile(profileName) {
  2950. if (!confirm(`Delete profile "${profileName}"? This will remove all entries for that profile.`)) return;
  2951. localStorage.removeItem('notifier.profile:' + profileName);
  2952. localStorage.removeItem(profileName + '.notifiers');
  2953. if (currentProfile === profileName) {
  2954. currentProfile = null;
  2955. }
  2956. if (!currentProfile) {
  2957. localStorage.removeItem('selectedProfile.notifier');
  2958. }
  2959. renderProfiles();
  2960. renderCalendar();
  2961. }
  2962.  
  2963. function saveSelectedProfile() {
  2964. if (currentProfile) {
  2965. localStorage.setItem('selectedProfile.notifier', currentProfile);
  2966. } else {
  2967. localStorage.removeItem('selectedProfile.notifier');
  2968. }
  2969. }
  2970.  
  2971. function getProfiles() {
  2972. let profiles = [];
  2973. for (let i=0; i<localStorage.length; i++) {
  2974. let key = localStorage.key(i);
  2975. if (key && key.startsWith('notifier.profile:')) {
  2976. let profileName = key.substring('notifier.profile:'.length);
  2977. profiles.push(profileName);
  2978. }
  2979. }
  2980. return profiles;
  2981. }
  2982.  
  2983. function renderProfiles() {
  2984. let profiles = getProfiles();
  2985. profileListEl.innerHTML = '';
  2986. profiles.forEach(p => {
  2987. let li = document.createElement('li');
  2988. let nameSpan = document.createElement('span');
  2989. nameSpan.className = 'profileName';
  2990. nameSpan.textContent = p;
  2991.  
  2992. let delBtn = document.createElement('button');
  2993. delBtn.className = 'deleteProfileBtn';
  2994. delBtn.textContent = 'Del';
  2995. delBtn.addEventListener('click', (e) => {
  2996. e.stopPropagation();
  2997. deleteProfile(p);
  2998. });
  2999.  
  3000. li.appendChild(nameSpan);
  3001. li.appendChild(delBtn);
  3002.  
  3003. if (p === currentProfile) {
  3004. li.classList.add('selected');
  3005. }
  3006. li.addEventListener('click', () => {
  3007. currentProfile = p;
  3008. saveSelectedProfile();
  3009. renderProfiles();
  3010. renderCalendar();
  3011. });
  3012. profileListEl.appendChild(li);
  3013. });
  3014. }
  3015.  
  3016. function getAllEntries() {
  3017. if (!currentProfile) return [];
  3018. let key = currentProfile + '.notifiers';
  3019. return JSON.parse(localStorage.getItem(key) || '[]');
  3020. }
  3021.  
  3022. function setAllEntries(entries) {
  3023. if (!currentProfile) return;
  3024. let key = currentProfile + '.notifiers';
  3025. localStorage.setItem(key, JSON.stringify(entries));
  3026. }
  3027.  
  3028. function renderCalendar() {
  3029. if (!currentProfile) {
  3030. currentMonthLabel.textContent = "No profile selected";
  3031. calendarGrid.innerHTML = '';
  3032. return;
  3033. }
  3034.  
  3035. let date = new Date(currentYear, currentMonth, 1);
  3036. let monthName = date.toLocaleString('default', { month: 'long' });
  3037. currentMonthLabel.textContent = `${monthName} ${currentYear}`;
  3038.  
  3039. let startDay = (date.getDay() + 6) % 7; // Adjusting so Monday=0
  3040. let daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
  3041.  
  3042. calendarGrid.innerHTML = '';
  3043.  
  3044. // Add empty cells for days before the first day of the month
  3045. for (let i = 0; i < startDay; i++) {
  3046. let emptyCell = document.createElement('div');
  3047. emptyCell.classList.add('calendarDay'); // Optional: Style empty cells similarly
  3048. calendarGrid.appendChild(emptyCell);
  3049. }
  3050.  
  3051. let entries = getAllEntries();
  3052.  
  3053. // Get today's date for highlighting
  3054. const today = new Date();
  3055. const todayYear = today.getFullYear();
  3056. const todayMonth = today.getMonth();
  3057. const todayDate = today.getDate();
  3058.  
  3059. for (let day = 1; day <= daysInMonth; day++) {
  3060. let cell = document.createElement('div');
  3061. cell.className = 'calendarDay';
  3062. cell.setAttribute('data-day', day);
  3063. cell.innerHTML = `<span class="dayNumber">${day}</span><div class="calendarDayEntries"></div>`;
  3064.  
  3065. // Highlight the current day
  3066. if (currentYear === todayYear && currentMonth === todayMonth && day === todayDate) {
  3067. cell.classList.add('currentDay');
  3068. }
  3069.  
  3070. cell.addEventListener('click', (e) => {
  3071. if (e.target.classList.contains('calendarEntry')) {
  3072. // Handled separately
  3073. } else {
  3074. openModalForNew(day);
  3075. }
  3076. });
  3077.  
  3078. let dayEntries = getEntriesForDay(entries, currentYear, currentMonth, day);
  3079. dayEntries.sort((a, b) => (a.startTime || '00:00').localeCompare(b.startTime || '00:00'));
  3080.  
  3081. let entriesContainer = cell.querySelector('.calendarDayEntries');
  3082. dayEntries.forEach(entry => {
  3083. let div = document.createElement('div');
  3084. div.className = 'calendarEntry';
  3085. div.setAttribute('draggable', 'true');
  3086. div.setAttribute('data-event-id', entry.id);
  3087.  
  3088. div.addEventListener('dragstart', (ev) => {
  3089. ev.dataTransfer.setData('text', entry.id);
  3090. });
  3091.  
  3092. div.addEventListener('click', (ev) => {
  3093. ev.stopPropagation();
  3094. openModalForEdit(entry.id);
  3095. });
  3096.  
  3097. div.textContent = formatEntry(entry);
  3098. entriesContainer.appendChild(div);
  3099. });
  3100.  
  3101. calendarGrid.appendChild(cell);
  3102. }
  3103. }
  3104.  
  3105.  
  3106. function isMultiDay(entry) {
  3107. return (entry.endYear !== entry.year || entry.endMonth !== entry.month || entry.endDay !== entry.day);
  3108. }
  3109.  
  3110. function dateInRange(date, start, end) {
  3111. return date >= start && date <= end;
  3112. }
  3113.  
  3114. function getEntriesForDay(allEntries, year, month, day) {
  3115. let dayDate = new Date(year, month, day);
  3116. let dayOfWeek = dayDate.getDay().toString();
  3117.  
  3118. return allEntries.filter(entry => {
  3119. let startDate = new Date(entry.year, entry.month, entry.day);
  3120. let endDate = new Date(entry.endYear, entry.endMonth, entry.endDay);
  3121.  
  3122. // If no recurrence
  3123. if (entry.recurrenceType === 'none') {
  3124. if (!dateInRange(dayDate, startDate, endDate)) return false;
  3125. return true;
  3126. }
  3127.  
  3128. // For all recurrence types, ensure day is after start date:
  3129. if (dayDate < startDate) return false;
  3130.  
  3131. if (entry.recurrenceType === 'daily') {
  3132. // If dailyDays is defined, only show on those days.
  3133. if (entry.dailyDays && entry.dailyDays.length > 0) {
  3134. return entry.dailyDays.includes(dayOfWeek);
  3135. }
  3136. // If no dailyDays stored, assume all days.
  3137. return true;
  3138. } else if (entry.recurrenceType === 'weekly') {
  3139. // If no days selected, we used start day. If days selected, must match.
  3140. return entry.weekDays && entry.weekDays.includes(dayOfWeek);
  3141. } else if (entry.recurrenceType === 'monthly') {
  3142. // Shows up every month on same day number.
  3143. return day === entry.day;
  3144. }
  3145.  
  3146. return false;
  3147. });
  3148. }
  3149.  
  3150.  
  3151. function formatEntry(entry) {
  3152. let timeStr = '';
  3153. if (entry.startTime && entry.endTime) {
  3154. timeStr = `${entry.startTime}-${entry.endTime} `;
  3155. } else if (entry.startTime) {
  3156. timeStr = `${entry.startTime} `;
  3157. }
  3158.  
  3159. let probStr = '';
  3160. if (entry.probability != null) {
  3161. probStr = `[${entry.probability}%] `;
  3162. }
  3163.  
  3164. return `${probStr}${timeStr}${entry.title}`;
  3165. }
  3166.  
  3167. function openModalForNew(day) {
  3168. editingEventId = null;
  3169. entryTitle.value = '';
  3170. entryStartTime.value = '';
  3171. entryEndTime.value = '';
  3172. recurrenceType.value = 'none';
  3173. weeklyDaysOptions.style.display = 'none';
  3174. dailyDaysOptions.style.display = 'none';
  3175. Array.from(weeklyDaysOptions.querySelectorAll('input[type=checkbox]')).forEach(cb => cb.checked = false);
  3176. entryProbability.value = 100;
  3177. probabilityValue.textContent = '100%';
  3178.  
  3179. let year = currentYear;
  3180. let month = currentMonth+1;
  3181. let dayPadded = String(day).padStart(2,'0');
  3182. let monthPadded = String(month).padStart(2,'0');
  3183. let dateStr = `${year}-${monthPadded}-${dayPadded}`;
  3184.  
  3185. entryStartDate.value = dateStr;
  3186. entryEndDate.value = dateStr;
  3187.  
  3188. deleteEntryBtn.style.display = 'none';
  3189.  
  3190. modal.style.display = 'flex';
  3191. }
  3192.  
  3193. function openModalForEdit(id) {
  3194. let entries = getAllEntries();
  3195. let entry = entries.find(e => e.id === id);
  3196. if (!entry) return;
  3197.  
  3198. editingEventId = id;
  3199.  
  3200. entryTitle.value = entry.title;
  3201. entryStartTime.value = entry.startTime;
  3202. entryEndTime.value = entry.endTime;
  3203. recurrenceType.value = entry.recurrenceType;
  3204. entryProbability.value = entry.probability || 100;
  3205. probabilityValue.textContent = (entry.probability || 100) + '%';
  3206.  
  3207. if (entry.recurrenceType === 'weekly') {
  3208. weeklyDaysOptions.style.display = 'block';
  3209. Array.from(weeklyDaysOptions.querySelectorAll('input[type=checkbox]')).forEach(cb => {
  3210. cb.checked = entry.weekDays && entry.weekDays.includes(cb.value);
  3211. });
  3212. } else {
  3213. weeklyDaysOptions.style.display = 'none';
  3214. }
  3215.  
  3216. if (entry.recurrenceType === 'daily') {
  3217. dailyDaysOptions.style.display = 'block';
  3218. Array.from(dailyDaysOptions.querySelectorAll('input[type=checkbox]')).forEach(cb => {
  3219. cb.checked = !entry.dailyDays || entry.dailyDays.includes(cb.value);
  3220. });
  3221. } else {
  3222. dailyDaysOptions.style.display = 'none';
  3223. }
  3224.  
  3225.  
  3226. let startY = entry.year; let startM = entry.month+1; let startD = entry.day;
  3227. let endY = entry.endYear; let endM = entry.endMonth+1; let endD = entry.endDay;
  3228. entryStartDate.value = `${startY}-${String(startM).padStart(2,'0')}-${String(startD).padStart(2,'0')}`;
  3229. entryEndDate.value = `${endY}-${String(endM).padStart(2,'0')}-${String(endD).padStart(2,'0')}`;
  3230.  
  3231. deleteEntryBtn.style.display = 'inline-block';
  3232.  
  3233. modal.style.display = 'flex';
  3234. }
  3235.  
  3236. function closeModal() {
  3237. modal.style.display = 'none';
  3238. }
  3239.  
  3240. function saveEntry() {
  3241. if (!currentProfile) return;
  3242.  
  3243. let title = entryTitle.value.trim();
  3244. if (!title) {
  3245. alert('Title cannot be empty.');
  3246. return;
  3247. }
  3248.  
  3249. let start = entryStartTime.value;
  3250. let end = entryEndTime.value;
  3251. let rType = recurrenceType.value;
  3252.  
  3253. let sDate = new Date(entryStartDate.value);
  3254. let eDate = new Date(entryEndDate.value);
  3255.  
  3256. // Validate start date
  3257. if (isNaN(sDate.getTime())) {
  3258. alert("Invalid start date");
  3259. return;
  3260. }
  3261.  
  3262. // If recurrence but no valid end date, pick a far future end date
  3263. if (rType !== 'none' && isNaN(eDate.getTime())) {
  3264. let futureDate = new Date(sDate);
  3265. futureDate.setFullYear(futureDate.getFullYear() + 100);
  3266. eDate = futureDate;
  3267. }
  3268.  
  3269. // If end date is given and is before start, warn
  3270. if (!isNaN(eDate.getTime()) && eDate < sDate) {
  3271. alert("End date must be after or equal to start date.");
  3272. return;
  3273. }
  3274.  
  3275. let prob = parseInt(entryProbability.value, 10) || 100;
  3276. let entries = getAllEntries();
  3277. let entry;
  3278.  
  3279. if (editingEventId) {
  3280. entry = entries.find(e => e.id === editingEventId);
  3281. if (!entry) return;
  3282. } else {
  3283. entry = { id: generateId() };
  3284. entries.push(entry);
  3285. }
  3286.  
  3287. entry.title = title;
  3288. entry.startTime = start || '';
  3289. entry.endTime = end || '';
  3290. entry.recurrenceType = rType;
  3291. entry.probability = prob;
  3292.  
  3293. entry.year = sDate.getFullYear();
  3294. entry.month = sDate.getMonth();
  3295. entry.day = sDate.getDate();
  3296. entry.endYear = eDate.getFullYear();
  3297. entry.endMonth = eDate.getMonth();
  3298. entry.endDay = eDate.getDate();
  3299.  
  3300. if (rType === 'weekly') {
  3301. let weekDays = Array.from(weeklyDaysOptions.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value);
  3302. if (weekDays.length === 0) {
  3303. let startDayOfWeek = sDate.getDay().toString();
  3304. weekDays = [startDayOfWeek];
  3305. }
  3306. entry.weekDays = weekDays;
  3307. delete entry.dailyDays; // ensure no dailyDays if weekly
  3308. } else if (rType === 'daily') {
  3309. // Gather selected daily days
  3310. let dailyDays = Array.from(dailyDaysOptions.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value);
  3311. // If all are checked, we can either store them all or consider that as a default 'all days'.
  3312. // Let's store them anyway.
  3313. if (dailyDays.length === 7) {
  3314. // All days selected. We can leave it as all.
  3315. // Storing them is fine, it means "all days".
  3316. }
  3317. entry.dailyDays = dailyDays;
  3318. delete entry.weekDays; // ensure no weekDays if daily
  3319. } else {
  3320. // no daily or weekly days
  3321. delete entry.weekDays;
  3322. delete entry.dailyDays;
  3323. }
  3324.  
  3325. setAllEntries(entries);
  3326. closeModal();
  3327. renderCalendar();
  3328. }
  3329.  
  3330.  
  3331. function deleteEntry(id) {
  3332. if (!confirm('Delete this entry?')) return;
  3333. let entries = getAllEntries();
  3334. entries = entries.filter(e => e.id !== id);
  3335. setAllEntries(entries);
  3336. closeModal();
  3337. renderCalendar();
  3338. }
  3339.  
  3340. function adjustRecurrenceForDrop(entry, dropDate) {
  3341. if (entry.recurrenceType === 'daily') {
  3342. let oldDate = new Date(entry.year, entry.month, entry.day);
  3343. let diff = (dropDate - oldDate)/(1000*3600*24);
  3344. let startObj = new Date(entry.year, entry.month, entry.day);
  3345. let endObj = new Date(entry.endYear, entry.endMonth, entry.endDay);
  3346. startObj.setDate(startObj.getDate()+diff);
  3347. endObj.setDate(endObj.getDate()+diff);
  3348. entry.year = startObj.getFullYear();
  3349. entry.month = startObj.getMonth();
  3350. entry.day = startObj.getDate();
  3351. entry.endYear = endObj.getFullYear();
  3352. entry.endMonth = endObj.getMonth();
  3353. entry.endDay = endObj.getDate();
  3354. } else if (entry.recurrenceType === 'weekly') {
  3355. let newDayOfWeek = dropDate.getDay().toString();
  3356. entry.weekDays = [newDayOfWeek];
  3357. let oldDate = new Date(entry.year, entry.month, entry.day);
  3358. let diff = (dropDate - oldDate)/(1000*3600*24);
  3359. let startObj = new Date(entry.year, entry.month, entry.day);
  3360. let endObj = new Date(entry.endYear, entry.endMonth, entry.endDay);
  3361. startObj.setDate(startObj.getDate()+diff);
  3362. endObj.setDate(endObj.getDate()+diff);
  3363. entry.year = startObj.getFullYear();
  3364. entry.month = startObj.getMonth();
  3365. entry.day = startObj.getDate();
  3366. entry.endYear = endObj.getFullYear();
  3367. entry.endMonth = endObj.getMonth();
  3368. entry.endDay = endObj.getDate();
  3369.  
  3370. } else if (entry.recurrenceType === 'monthly') {
  3371. let oldStart = new Date(entry.year, entry.month, entry.day);
  3372. let oldEnd = new Date(entry.endYear, entry.endMonth, entry.endDay);
  3373. let length = (oldEnd - oldStart)/(1000*3600*24);
  3374. entry.year = dropDate.getFullYear();
  3375. entry.month = dropDate.getMonth();
  3376. entry.day = dropDate.getDate();
  3377. let endObj = new Date(entry.year, entry.month, entry.day);
  3378. endObj.setDate(endObj.getDate()+length);
  3379. entry.endYear = endObj.getFullYear();
  3380. entry.endMonth = endObj.getMonth();
  3381. entry.endDay = endObj.getDate();
  3382. } else {
  3383. let oldDate = new Date(entry.year, entry.month, entry.day);
  3384. let diff = (dropDate - oldDate)/(1000*3600*24);
  3385. let startObj = new Date(entry.year, entry.month, entry.day);
  3386. let endObj = new Date(entry.endYear, entry.endMonth, entry.endDay);
  3387. startObj.setDate(startObj.getDate()+diff);
  3388. endObj.setDate(endObj.getDate()+diff);
  3389. entry.year = startObj.getFullYear();
  3390. entry.month = startObj.getMonth();
  3391. entry.day = startObj.getDate();
  3392. entry.endYear = endObj.getFullYear();
  3393. entry.endMonth = endObj.getMonth();
  3394. entry.endDay = endObj.getDate();
  3395. }
  3396. }
  3397.  
  3398. let messageSentTimestamps = {};
  3399. //let notifierActive = false;
  3400.  
  3401. function loadNotifiers(profileName) {
  3402. console.log("Loading notifiers for profile:", profileName);
  3403. const data = localStorage.getItem(`${profileName}.notifiers`);
  3404. if (!data) {
  3405. console.log("No notifier data found for profile:", profileName);
  3406. return [];
  3407. }
  3408. try {
  3409. const parsed = JSON.parse(data);
  3410. console.log("Notifier data loaded:", parsed);
  3411. return parsed;
  3412. } catch (e) {
  3413. console.error("Failed to parse notifier data:", e);
  3414. return [];
  3415. }
  3416. }
  3417.  
  3418. function saveMessageSentTimestamps() {
  3419. localStorage.setItem('messageSentTimestamps', JSON.stringify(messageSentTimestamps));
  3420. }
  3421.  
  3422. function loadMessageSentTimestamps() {
  3423. console.log("Loading messageSentTimestamps...");
  3424. messageSentTimestamps = JSON.parse(localStorage.getItem('messageSentTimestamps') || '{}');
  3425. console.log("messageSentTimestamps loaded:", messageSentTimestamps);
  3426. }
  3427.  
  3428. function getRandomInt(min, max) {
  3429. return Math.floor(Math.random() * (max - min + 1)) + min;
  3430. }
  3431.  
  3432. // Get ISO week number
  3433. function getWeekNumber(date) {
  3434. const tempDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  3435. const dayNum = tempDate.getUTCDay() || 7;
  3436. tempDate.setUTCDate(tempDate.getUTCDate() + 4 - dayNum);
  3437. const yearStart = new Date(Date.UTC(tempDate.getUTCFullYear(),0,1));
  3438. return Math.ceil((((tempDate - yearStart) / 86400000) + 1)/7);
  3439. }
  3440.  
  3441. function isSameDay(date1, date2) {
  3442. return date1.getFullYear() === date2.getFullYear() &&
  3443. date1.getMonth() === date2.getMonth() &&
  3444. date1.getDate() === date2.getDate();
  3445. }
  3446.  
  3447. // For monthly triggers
  3448. function getMonthlyTriggerDate(event, now) {
  3449. console.log("Determining monthly trigger date for event:", event.id);
  3450. const yearMonthKey = `${event.id}-monthlyTrigger-${now.getFullYear()}-${now.getMonth()}`;
  3451. let stored = localStorage.getItem(yearMonthKey);
  3452. if (stored) {
  3453. console.log("Found stored monthly trigger date/time:", stored);
  3454. return JSON.parse(stored);
  3455. }
  3456.  
  3457. const year = now.getFullYear();
  3458. const month = now.getMonth();
  3459. const daysInMonth = new Date(year, month+1, 0).getDate();
  3460.  
  3461. let day = getRandomInt(1, daysInMonth);
  3462. let hour = 0, minute = 0;
  3463. if (event.endTime) {
  3464. const [startH, startM] = event.startTime.split(':').map(Number);
  3465. const [endH, endM] = event.endTime.split(':').map(Number);
  3466. const startTotal = startH*60 + startM;
  3467. const endTotal = endH*60 + endM;
  3468. const randomTotal = getRandomInt(startTotal, endTotal);
  3469. hour = Math.floor(randomTotal / 60);
  3470. minute = randomTotal % 60;
  3471. } else {
  3472. const [h,m] = event.startTime.split(':').map(Number);
  3473. hour = h; minute = m;
  3474. }
  3475.  
  3476. const triggerData = { day, hour, minute };
  3477. localStorage.setItem(yearMonthKey, JSON.stringify(triggerData));
  3478. console.log("Stored new monthly trigger date/time:", triggerData);
  3479. return triggerData;
  3480. }
  3481.  
  3482. // For weekly triggers
  3483. function getWeeklyTriggerTime(event, now) {
  3484. console.log("Determining weekly trigger time for event:", event.id);
  3485. const year = now.getFullYear();
  3486. const weekNum = getWeekNumber(now);
  3487. const dow = now.getDay();
  3488. const key = `${event.id}-weeklyTrigger-${year}-${weekNum}-${dow}`;
  3489.  
  3490. let stored = localStorage.getItem(key);
  3491. if (stored) {
  3492. console.log("Found stored weekly trigger time:", stored);
  3493. return JSON.parse(stored);
  3494. }
  3495.  
  3496. if (!event.endTime) {
  3497. const [h,m] = event.startTime.split(':').map(Number);
  3498. const triggerData = { hour: h, minute: m };
  3499. localStorage.setItem(key, JSON.stringify(triggerData));
  3500. console.log("No timeframe, weekly trigger time:", triggerData);
  3501. return triggerData;
  3502. }
  3503.  
  3504. const [startH, startM] = event.startTime.split(':').map(Number);
  3505. const [endH, endM] = event.endTime.split(':').map(Number);
  3506. const startTotal = startH*60 + startM;
  3507. const endTotal = endH*60 + endM;
  3508. const randomTotal = getRandomInt(startTotal, endTotal);
  3509. const hour = Math.floor(randomTotal / 60);
  3510. const minute = randomTotal % 60;
  3511. const triggerData = { hour, minute };
  3512. localStorage.setItem(key, JSON.stringify(triggerData));
  3513. console.log("Timeframe weekly trigger time:", triggerData);
  3514. return triggerData;
  3515. }
  3516.  
  3517. // For daily triggers
  3518. function getDailyTriggerTime(event, now) {
  3519. console.log("Determining daily trigger time for event:", event.id);
  3520. const dateKey = `${event.id}-dailyTrigger-${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
  3521. let stored = localStorage.getItem(dateKey);
  3522. if (stored) {
  3523. console.log("Found stored daily trigger time:", stored);
  3524. return JSON.parse(stored);
  3525. }
  3526.  
  3527. if (!event.endTime) {
  3528. const [h,m] = event.startTime.split(':').map(Number);
  3529. const triggerData = { hour: h, minute: m };
  3530. localStorage.setItem(dateKey, JSON.stringify(triggerData));
  3531. console.log("No timeframe, daily trigger time:", triggerData);
  3532. return triggerData;
  3533. }
  3534.  
  3535. const [startH, startM] = event.startTime.split(':').map(Number);
  3536. const [endH, endM] = event.endTime.split(':').map(Number);
  3537. const startTotal = startH*60 + startM;
  3538. const endTotal = endH*60 + endM;
  3539. const randomTotal = getRandomInt(startTotal, endTotal);
  3540. const hour = Math.floor(randomTotal / 60);
  3541. const minute = randomTotal % 60;
  3542. const triggerData = { hour, minute };
  3543. localStorage.setItem(dateKey, JSON.stringify(triggerData));
  3544. console.log("Timeframe daily trigger time:", triggerData);
  3545. return triggerData;
  3546. }
  3547.  
  3548. // For single events
  3549. function getSingleTriggerTime(event) {
  3550. console.log("Determining single event trigger time:", event.id);
  3551. const eventDateKey = `${event.id}-singleTrigger-${event.year}-${event.month}-${event.day}`;
  3552. let stored = localStorage.getItem(eventDateKey);
  3553. if (stored) {
  3554. console.log("Found stored single trigger time:", stored);
  3555. return JSON.parse(stored);
  3556. }
  3557.  
  3558. if (!event.endTime) {
  3559. const [h,m] = event.startTime.split(':').map(Number);
  3560. const triggerData = { hour: h, minute: m };
  3561. localStorage.setItem(eventDateKey, JSON.stringify(triggerData));
  3562. console.log("No timeframe, single event trigger time:", triggerData);
  3563. return triggerData;
  3564. }
  3565.  
  3566. const [startH, startM] = event.startTime.split(':').map(Number);
  3567. const [endH, endM] = event.endTime.split(':').map(Number);
  3568. const startTotal = startH*60 + startM;
  3569. const endTotal = endH*60 + endM;
  3570. const randomTotal = getRandomInt(startTotal, endTotal);
  3571. const hour = Math.floor(randomTotal / 60);
  3572. const minute = randomTotal % 60;
  3573.  
  3574. const triggerData = { hour, minute };
  3575. localStorage.setItem(eventDateKey, JSON.stringify(triggerData));
  3576. console.log("Timeframe single event trigger time:", triggerData);
  3577. return triggerData;
  3578. }
  3579.  
  3580. function hasTriggeredThisPeriod(event, now) {
  3581. const key = getTriggerKeyForEvent(event, now);
  3582. const triggered = !!localStorage.getItem(key);
  3583. console.log(`Checking if event ${event.id} triggered this period (key: ${key}):`, triggered);
  3584. return triggered;
  3585. }
  3586.  
  3587. function markTriggeredThisPeriod(event, now) {
  3588. const key = getTriggerKeyForEvent(event, now);
  3589. localStorage.setItem(key, "triggered");
  3590. console.log(`Marked event ${event.id} as triggered for period with key:`, key);
  3591. }
  3592.  
  3593. function getTriggerKeyForEvent(event, now) {
  3594. const year = now.getFullYear();
  3595. const month = now.getMonth();
  3596. const date = now.getDate();
  3597. switch (event.recurrenceType) {
  3598. case 'daily':
  3599. return `${event.id}-triggered-${year}-${month}-${date}`;
  3600. case 'weekly': {
  3601. const weekNum = getWeekNumber(now);
  3602. return `${event.id}-triggered-${year}-week${weekNum}`;
  3603. }
  3604. case 'monthly':
  3605. return `${event.id}-triggered-${year}-${month}`;
  3606. case 'none':
  3607. case '':
  3608. case undefined:
  3609. case null:
  3610. // single/no recurrence
  3611. if (event.year !== undefined && event.month !== undefined && event.day !== undefined) {
  3612. return `${event.id}-triggered-${event.year}-${event.month}-${event.day}`;
  3613. } else {
  3614. // no date means trigger once ever
  3615. return `${event.id}-triggered-once`;
  3616. }
  3617. default:
  3618. // Unknown recurrence - treat as single event to avoid skipping
  3619. return `${event.id}-triggered-once`;
  3620. }
  3621. }
  3622.  
  3623. function isEventDueThisPeriod(event, now) {
  3624. console.log("Checking if event is due this period:", event.id, "Recurrence:", event.recurrenceType);
  3625. if (!event.recurrenceType || event.recurrenceType === "" || event.recurrenceType === "none") {
  3626. // single event logic
  3627. if (event.year !== undefined && event.month !== undefined && event.day !== undefined) {
  3628. const evDate = new Date(event.year, event.month, event.day);
  3629. const due = isSameDay(evDate, now);
  3630. console.log("Single event due check:", due);
  3631. return due;
  3632. } else {
  3633. // If no specific date is given and no recurrence, assume it should trigger today
  3634. // This means we just consider it could trigger any time today
  3635. return true;
  3636. }
  3637. }
  3638.  
  3639. if (event.recurrenceType === 'daily') {
  3640. console.log("Daily event is always due");
  3641. return true;
  3642. }
  3643.  
  3644. if (event.recurrenceType === 'weekly') {
  3645. if (!Array.isArray(event.weekDays) || event.weekDays.length === 0) {
  3646. console.log("Weekly event has no weekDays defined, not due");
  3647. return false;
  3648. }
  3649. const currentDayOfWeek = now.getDay();
  3650. const dayInts = event.weekDays.map(d => parseInt(d,10));
  3651. const due = dayInts.includes(currentDayOfWeek);
  3652. console.log("Weekly event due check (currentDayOfWeek:", currentDayOfWeek, "):", due);
  3653. return due;
  3654. }
  3655.  
  3656. if (event.recurrenceType === 'monthly') {
  3657. // monthly triggers once a month
  3658. console.log("Monthly event is due if not triggered yet this month");
  3659. return true;
  3660. }
  3661.  
  3662. console.log("Recurrence type unknown:", event.recurrenceType);
  3663. return false;
  3664. }
  3665.  
  3666. function probabilityPassed(prob) {
  3667. const roll = Math.random() * 100;
  3668. console.log("Probability roll:", roll, "Needed:", prob);
  3669. return roll <= prob;
  3670. }
  3671.  
  3672. function tryTriggerEvent(event, now) {
  3673. console.log("Trying to trigger event:", event.id, event);
  3674. if (hasTriggeredThisPeriod(event, now)) {
  3675. console.log("Event already triggered this period, skipping:", event.id);
  3676. return false;
  3677. }
  3678.  
  3679. if (!isEventDueThisPeriod(event, now)) {
  3680. console.log("Event not due this period, skipping:", event.id);
  3681. return false;
  3682. }
  3683.  
  3684. let triggerHour, triggerMinute, triggerDay = now.getDate();
  3685.  
  3686. // Determine trigger time based on recurrence and timeframe
  3687. if (event.recurrenceType === 'daily') {
  3688. const trig = getDailyTriggerTime(event, now);
  3689. triggerHour = trig.hour;
  3690. triggerMinute = trig.minute;
  3691. } else if (event.recurrenceType === 'weekly') {
  3692. const trig = getWeeklyTriggerTime(event, now);
  3693. triggerHour = trig.hour;
  3694. triggerMinute = trig.minute;
  3695. } else if (event.recurrenceType === 'monthly') {
  3696. const trig = getMonthlyTriggerDate(event, now);
  3697. triggerHour = trig.hour;
  3698. triggerMinute = trig.minute;
  3699. triggerDay = trig.day;
  3700. } else {
  3701. // single/no recurrence
  3702. const trig = getSingleTriggerTime(event);
  3703. triggerHour = trig.hour;
  3704. triggerMinute = trig.minute;
  3705. }
  3706.  
  3707. console.log("Trigger time determined:", {triggerDay, triggerHour, triggerMinute});
  3708.  
  3709. if (event.recurrenceType === 'monthly' && triggerDay !== now.getDate()) {
  3710. console.log("Not the chosen day for monthly event. Current day:", now.getDate(), "Trigger day:", triggerDay);
  3711. return false;
  3712. }
  3713.  
  3714. if (now.getHours() === triggerHour && now.getMinutes() === triggerMinute) {
  3715. let prob = event.probability !== undefined ? event.probability : 100;
  3716. if (!probabilityPassed(prob)) {
  3717. console.log("Probability check failed for event:", event.id);
  3718. markTriggeredThisPeriod(event, now);
  3719. return false;
  3720. }
  3721.  
  3722. console.log("Event conditions met, triggering event:", event.title);
  3723. sendNotifierMessage(`<[Notifier] ${event.title}>`);
  3724. markTriggeredThisPeriod(event, now);
  3725. return true;
  3726. } else {
  3727. console.log("Current time does not match trigger time for event:", event.id);
  3728. }
  3729.  
  3730. return false;
  3731. }
  3732.  
  3733. function sendNotifierMessage(notifierMessage) {
  3734. console.log("Sending notifier message:", notifierMessage);
  3735. const inputElement = document.querySelector('[data-slate-editor="true"]') || document.querySelector('textarea[class*="textArea_"]');
  3736. if (!inputElement) {
  3737. console.log("No input element found, cannot send message");
  3738. return;
  3739. }
  3740.  
  3741. notifierActive = true;
  3742.  
  3743. inputElement.focus();
  3744. if (inputElement.getAttribute('data-slate-editor') === 'true') {
  3745. const inputEvent = new InputEvent('beforeinput', {
  3746. bubbles: true,
  3747. cancelable: true,
  3748. inputType: 'insertText',
  3749. data: `${notifierMessage}`,
  3750. });
  3751. inputElement.dispatchEvent(inputEvent);
  3752. } else {
  3753. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  3754. nativeInputValueSetter.call(inputElement, inputElement.value + notifierMessage);
  3755.  
  3756. const inputEvent = new Event('input', {
  3757. bubbles: true,
  3758. cancelable: true,
  3759. });
  3760. inputElement.dispatchEvent(inputEvent);
  3761. inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
  3762. }
  3763.  
  3764. let sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  3765. if (sendButton) {
  3766. sendButton.click();
  3767. console.log('Send button clicked to send message');
  3768. notifierActive = false; // Reset notifierActive after sending
  3769. } else {
  3770. // For desktop version, simulate pressing Enter to send the message
  3771. let enterEvent = new KeyboardEvent('keydown', {
  3772. key: 'Enter',
  3773. code: 'Enter',
  3774. keyCode: 13,
  3775. which: 13,
  3776. bubbles: true,
  3777. cancelable: true
  3778. });
  3779. inputElement.dispatchEvent(enterEvent);
  3780. console.log('Enter key simulated to send message');
  3781. notifierActive = false; // Reset notifierActive after sending
  3782. }
  3783. }
  3784.  
  3785. function setInputValue() {
  3786. console.log("setInputValue() called");
  3787. if (notifierActive) {
  3788. console.log("notifierActive is true, skipping");
  3789. return;
  3790. }
  3791.  
  3792. const selectedProfile = localStorage.getItem('selectedProfile.notifier');
  3793. if (!selectedProfile) {
  3794. console.error("Selected profile not found in localStorage");
  3795. return;
  3796. }
  3797.  
  3798. loadMessageSentTimestamps();
  3799.  
  3800. const now = new Date();
  3801. const notifiers = loadNotifiers(selectedProfile);
  3802. if (!notifiers || notifiers.length === 0) {
  3803. console.log("No notifiers found for this profile");
  3804. return;
  3805. }
  3806.  
  3807. console.log("Checking events for triggering...");
  3808. for (let event of notifiers) {
  3809. if (tryTriggerEvent(event, now)) {
  3810. console.log("Event triggered, stopping further checks");
  3811. break;
  3812. }
  3813. }
  3814. }
  3815.  
  3816. function checkForMessages() {
  3817. console.log("Starting interval to check for messages every 20s");
  3818. setInterval(() => {
  3819. setInputValue();
  3820. }, 20000);
  3821. }
  3822.  
  3823. // Throttle function to limit the frequency of callback execution
  3824. function throttle(callback, limit) {
  3825. let waiting = false; // Initially, we're not waiting
  3826. return function (...args) {
  3827. if (!waiting) {
  3828. callback.apply(this, args); // Execute callback
  3829. waiting = true; // Prevent further execution
  3830. setTimeout(() => (waiting = false), limit); // Reset waiting after limit
  3831. }
  3832. };
  3833. }
  3834.  
  3835. // Throttle the setInputValue logic
  3836. const throttledCallback = throttle(() => {
  3837. console.log("Mutation observed, attempting setInputValue()");
  3838. if (document.querySelector('[data-slate-editor="true"]') || document.querySelector('textarea[class*="textArea_"]')) {
  3839. setInputValue();
  3840. } else {
  3841. console.log("No input field found in mutation observer");
  3842. }
  3843. }, 2000); // Limit to 200ms intervals (adjust as needed)
  3844.  
  3845. const observer = new MutationObserver(throttledCallback);
  3846.  
  3847. observer.observe(document.body, { childList: true, subtree: true });
  3848.  
  3849.  
  3850. checkForMessages();
  3851.  
  3852. console.log("Notifier script loaded and running...");
  3853.  
  3854.  
  3855. }
  3856.  
  3857. // ================================================================= MAIN LOGIC =================================================================
  3858. function MainLogicScript() {
  3859. // Function to check localStorage and reload if not ready
  3860. function checkLocalStorageAndReload() {
  3861. try {
  3862. if (localStorage.length > 0) {
  3863. console.log("LocalStorage has items. Proceeding with script...");
  3864. initializeScript();
  3865. } else {
  3866. console.warn("LocalStorage is empty. Reloading page...");
  3867. setTimeout(() => {
  3868. location.reload();
  3869. }, 5000); // Wait 5 seconds before reloading
  3870. }
  3871. } catch (error) {
  3872. console.error("Error accessing localStorage:", error);
  3873. setTimeout(() => {
  3874. location.reload();
  3875. }, 5000); // Wait 5 seconds before reloading
  3876. }
  3877. }
  3878.  
  3879. // Initial check for localStorage existence
  3880. checkLocalStorageAndReload();
  3881.  
  3882. function initializeScript() {
  3883. // Retrieve settings from localStorage or set default values
  3884. let enterKeyDisabled = JSON.parse(localStorage.getItem('enterKeyDisabled')) || false;
  3885. let customRuleEnabled = JSON.parse(localStorage.getItem('customRuleEnabled')) || true;
  3886. let scanForKeywordsEnabled = JSON.parse(localStorage.getItem('scanForKeywordsEnabled')) || true;
  3887.  
  3888. // Create the settings window
  3889. window.settingsWindow = document.createElement('div');
  3890. settingsWindow.style.bottom = '60px';
  3891. settingsWindow.style.right = '20px';
  3892. settingsWindow.style.width = '250px';
  3893. settingsWindow.style.padding = '15px';
  3894. settingsWindow.style.color = 'white';
  3895. settingsWindow.style.borderRadius = '5px';
  3896. settingsWindow.style.zIndex = '1001';
  3897.  
  3898. // Custom Rule Checkbox
  3899. const enableCustomRuleCheckbox = document.createElement('input');
  3900. enableCustomRuleCheckbox.type = 'checkbox';
  3901. enableCustomRuleCheckbox.checked = customRuleEnabled;
  3902. enableCustomRuleCheckbox.id = 'enableCustomRuleCheckbox';
  3903.  
  3904. const enableCustomRuleLabel = document.createElement('label');
  3905. enableCustomRuleLabel.htmlFor = 'enableCustomRuleCheckbox';
  3906. enableCustomRuleLabel.innerText = ' Enable Custom Rules';
  3907. enableCustomRuleLabel.style.color = '#b0b0b0';
  3908.  
  3909. // Wrap Custom Rule elements in a div
  3910. const customRuleDiv = document.createElement('div');
  3911. customRuleDiv.style.marginBottom = '10px'; // Add spacing
  3912. customRuleDiv.appendChild(enableCustomRuleCheckbox);
  3913. customRuleDiv.appendChild(enableCustomRuleLabel);
  3914.  
  3915. // Scan for Keywords Checkbox
  3916. const enableScanForKeywordsCheckbox = document.createElement('input');
  3917. enableScanForKeywordsCheckbox.type = 'checkbox';
  3918. enableScanForKeywordsCheckbox.checked = scanForKeywordsEnabled;
  3919. enableScanForKeywordsCheckbox.id = 'enableScanForKeywordsCheckbox';
  3920.  
  3921. const enableScanForKeywordsLabel = document.createElement('label');
  3922. enableScanForKeywordsLabel.htmlFor = 'enableScanForKeywordsCheckbox';
  3923. enableScanForKeywordsLabel.innerText = ' Enable Lorebook';
  3924. enableScanForKeywordsLabel.style.color = '#b0b0b0';
  3925.  
  3926. // Wrap Scan for Keywords elements in a div
  3927. const scanForKeywordsDiv = document.createElement('div');
  3928. scanForKeywordsDiv.style.marginBottom = '10px'; // Add spacing
  3929. scanForKeywordsDiv.appendChild(enableScanForKeywordsCheckbox);
  3930. scanForKeywordsDiv.appendChild(enableScanForKeywordsLabel);
  3931.  
  3932. // Append elements to settings window
  3933. settingsWindow.appendChild(customRuleDiv);
  3934. settingsWindow.appendChild(scanForKeywordsDiv);
  3935.  
  3936.  
  3937. // Update customRuleEnabled when checkbox is toggled, and save it in localStorage
  3938. enableCustomRuleCheckbox.addEventListener('change', function() {
  3939. customRuleEnabled = enableCustomRuleCheckbox.checked;
  3940. localStorage.setItem('customRuleEnabled', JSON.stringify(customRuleEnabled));
  3941. });
  3942.  
  3943. // Update scanForKeywordsEnabled when checkbox is toggled, and save it in localStorage
  3944. enableScanForKeywordsCheckbox.addEventListener('change', function() {
  3945. scanForKeywordsEnabled = enableScanForKeywordsCheckbox.checked;
  3946. localStorage.setItem('scanForKeywordsEnabled', JSON.stringify(scanForKeywordsEnabled));
  3947. });
  3948.  
  3949.  
  3950. // Function to get the correct input element based on the mode
  3951. function getInputElement() {
  3952. return document.querySelector('[data-slate-editor="true"]') || document.querySelector('textarea[class*="textArea_"]');
  3953. }
  3954.  
  3955. // Add event listener to handle Enter key behavior
  3956. window.addEventListener('keydown', function(event) {
  3957. if (notifierActive) return;
  3958.  
  3959. const inputElement = getInputElement();
  3960. if (event.key === 'Enter' && !event.shiftKey && !enterKeyDisabled) {
  3961. if (inputElement && inputElement.nodeName === 'TEXTAREA') {
  3962. return;
  3963. }
  3964.  
  3965. event.preventDefault();
  3966. event.stopPropagation();
  3967. event.stopImmediatePropagation();
  3968. console.log('Enter key disabled');
  3969. enterKeyDisabled = true;
  3970.  
  3971. handleEnterKey();
  3972.  
  3973. enterKeyDisabled = false;
  3974. }
  3975. }, true);
  3976.  
  3977.  
  3978. // Add event listener to the send button to execute handleEnterKey when clicked
  3979. window.addEventListener('click', function(event) {
  3980. if (notifierActive) {
  3981. return; // Skip handling if the notifier is active
  3982. }
  3983. const sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  3984. const inputElement = getInputElement();
  3985.  
  3986. if (sendButton && sendButton.contains(event.target)) {
  3987. if (inputElement && inputElement.nodeName === 'TEXTAREA') {
  3988. // Skip handleEnterKey() if notifier is active
  3989. if (!notifierActive) {
  3990. handleEnterKey();
  3991. }
  3992. }
  3993. }
  3994. }, true);
  3995.  
  3996.  
  3997.  
  3998. // Main function that handles Enter key behavior
  3999. function handleEnterKey() {
  4000. if (notifierActive) {
  4001. console.log('Notifier is active, skipping handleEnterKey');
  4002. return; // Prevent handleEnterKey from executing if notifier is active
  4003. }
  4004.  
  4005. let inputElement = getInputElement();
  4006. if (inputElement) {
  4007. inputElement.focus();
  4008. setCursorToEnd(inputElement);
  4009. if (customRuleEnabled) {
  4010. applyCustomRule(inputElement);
  4011. }
  4012. setCursorToEnd(inputElement);
  4013. if (scanForKeywordsEnabled) {
  4014. scanForKeywords(inputElement);
  4015. }
  4016. getRandomEntry(inputElement);
  4017. sendMessage(inputElement);
  4018. anotherCustomFunction();
  4019. }
  4020. }
  4021.  
  4022.  
  4023. // Function to apply custom rules for the input field
  4024. function applyCustomRule(inputElement) {
  4025. const customRule = window.customRuleLogic ? window.customRuleLogic.getCurrentText() : '';
  4026.  
  4027. if (inputElement.nodeName === 'TEXTAREA') {
  4028. // For mobile version where input is <textarea>
  4029. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  4030. nativeInputValueSetter.call(inputElement, inputElement.value + customRule);
  4031.  
  4032. const inputEvent = new Event('input', {
  4033. bubbles: true,
  4034. cancelable: true,
  4035. });
  4036. inputElement.dispatchEvent(inputEvent);
  4037. } else {
  4038. // For desktop version where input is a Slate editor
  4039. const inputEvent = new InputEvent('beforeinput', {
  4040. bubbles: true,
  4041. cancelable: true,
  4042. inputType: 'insertText',
  4043. data: customRule,
  4044. });
  4045. inputElement.dispatchEvent(inputEvent);
  4046. }
  4047. }
  4048.  
  4049. // Function to set the cursor position to the end after inserting the text
  4050. function setCursorToEnd(inputElement) {
  4051. if (inputElement.nodeName === 'TEXTAREA') {
  4052. // For mobile version where input is <textarea>
  4053. inputElement.selectionStart = inputElement.selectionEnd = inputElement.value.length;
  4054. } else {
  4055. // For desktop version where input is a Slate editor
  4056. inputElement.focus();
  4057.  
  4058. // Simulate repeated Ctrl + ArrowRight key press events to move cursor to the end
  4059. const repeatPresses = 150; // Number of times to simulate Ctrl + ArrowRight
  4060. for (let i = 0; i < repeatPresses; i++) {
  4061. const ctrlArrowRightEvent = new KeyboardEvent('keydown', {
  4062. key: 'ArrowRight',
  4063. code: 'ArrowRight',
  4064. keyCode: 39, // ArrowRight key code
  4065. charCode: 0,
  4066. which: 39,
  4067. bubbles: true,
  4068. cancelable: true,
  4069. ctrlKey: true // Set Ctrl key to true
  4070. });
  4071. inputElement.dispatchEvent(ctrlArrowRightEvent);
  4072. }
  4073. }
  4074. }
  4075.  
  4076. // Function to send the message (either click send button or simulate Enter key)
  4077. function sendMessage(inputElement) {
  4078. if (inputElement.nodeName === 'TEXTAREA') {
  4079. // Mobile version: Do not send message, just return to allow linebreak
  4080. return;
  4081. }
  4082.  
  4083. let sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  4084. if (sendButton) {
  4085. sendButton.click();
  4086. console.log('Send button clicked to send message');
  4087. } else {
  4088. // For desktop version, simulate pressing Enter to send the message
  4089. let enterEvent = new KeyboardEvent('keydown', {
  4090. key: 'Enter',
  4091. code: 'Enter',
  4092. keyCode: 13,
  4093. which: 13,
  4094. bubbles: true,
  4095. cancelable: true
  4096. });
  4097. inputElement.dispatchEvent(enterEvent);
  4098. console.log('Enter key simulated to send message');
  4099. }
  4100. }
  4101.  
  4102.  
  4103.  
  4104. // Example of adding another function
  4105. function anotherCustomFunction() {
  4106. console.log('Another custom function executed');
  4107. }
  4108.  
  4109.  
  4110.  
  4111.  
  4112. // Function to get the current profile from local storage
  4113. function getCurrentProfile() {
  4114. return localStorage.getItem('selectedProfile.lorebook');
  4115. }
  4116.  
  4117.  
  4118.  
  4119. // Function to scan for keywords and access local storage
  4120. function scanForKeywords(inputElement) {
  4121. const currentProfile = getCurrentProfile();
  4122. if (currentProfile) {
  4123. // Retrieve all messages before iterating through storage keys
  4124. const messageItems = document.querySelectorAll('div[class*="messageContent_"]');
  4125. let relevantMessages = Array.from(messageItems).slice(-15); // Last 15 messages
  4126. const lastMessage = Array.from(messageItems).slice(-1); // Last message only
  4127.  
  4128.  
  4129. // Iterate over the last 15 messages to extract hidden bracket content
  4130. relevantMessages = relevantMessages.map(msg => {
  4131. // Retrieve all span elements within the message
  4132. const spans = msg.querySelectorAll('span');
  4133.  
  4134. // Filter out the spans based on both style conditions: opacity and position
  4135. const hiddenSpans = Array.from(spans).filter(span => {
  4136. const style = window.getComputedStyle(span);
  4137. return style.opacity === '0' && style.position === 'absolute';
  4138. });
  4139.  
  4140. // Join the text content of all matching spans, converting them to lowercase
  4141. const bracketContent = hiddenSpans.map(span => span.textContent.toLowerCase()).join('');
  4142.  
  4143. // Extract content within square brackets, if any
  4144. const match = bracketContent.match(/\[(.*?)\]/);
  4145. return match ? match[1] : null;
  4146. }).filter(content => content !== null);
  4147.  
  4148. // Log the filtered messages for debugging purposes
  4149. console.log("Filtered Relevant Messages (content in brackets, last 15):", relevantMessages);
  4150. console.log("Last Message:", lastMessage.map(msg => msg.textContent));
  4151.  
  4152. // Track how many entries have been appended
  4153. let appendedCount = 0;
  4154. const maxAppends = 3;
  4155.  
  4156. // Check if the last message contains a specific link pattern
  4157. const mp3LinkPattern = /https:\/\/files\.shapes\.inc\/.*\.mp3/;
  4158. let mp3LinkValue = null;
  4159.  
  4160. if (lastMessage.length > 0) {
  4161. const lastMessageText = lastMessage[0].textContent.toLowerCase();
  4162.  
  4163. const mp3LinkMatch = lastMessageText.match(mp3LinkPattern);
  4164. if (mp3LinkMatch) {
  4165. const mp3LinkKey = mp3LinkMatch[0];
  4166. mp3LinkValue = localStorage.getItem(mp3LinkKey);
  4167. console.log(`MP3 Link detected: ${mp3LinkKey}. Retrieved value: ${mp3LinkValue}`);
  4168. }
  4169. }
  4170.  
  4171. // Create an array to hold all entry keys that need to be checked
  4172. let allEntryKeys = [];
  4173.  
  4174. // Iterate through all localStorage keys that match the profile-lorebook prefix
  4175. Object.keys(localStorage).forEach(storageKey => {
  4176. if (storageKey.startsWith(`${currentProfile}.lorebook:`)) {
  4177. const entryKeys = storageKey.replace(`${currentProfile}.lorebook:`, '').split(',');
  4178. const entryValue = localStorage.getItem(storageKey);
  4179.  
  4180. // Log the entry keys for debugging purposes
  4181. console.log(`Entry Keys: `, entryKeys);
  4182.  
  4183. entryKeys.forEach(entryKey => {
  4184. allEntryKeys.push({ entryKey, entryValue });
  4185. });
  4186. }
  4187. });
  4188.  
  4189. // If mp3LinkValue is present, parse it for keywords as well
  4190. if (mp3LinkValue) {
  4191. console.log(`Scanning MP3 link value for keywords: ${mp3LinkValue}`);
  4192. const mp3Keywords = mp3LinkValue.split(',');
  4193. mp3Keywords.forEach(keyword => {
  4194. const trimmedKeyword = keyword.trim();
  4195. console.log(`Adding keyword from MP3 value: ${trimmedKeyword}`);
  4196. // Add mp3 keywords but set entryValue to an empty string instead of null
  4197. allEntryKeys.push({ entryKey: trimmedKeyword, entryValue: '' });
  4198. });
  4199. }
  4200.  
  4201. // Iterate over all collected entry keys and perform the checks
  4202. allEntryKeys.forEach(({ entryKey, entryValue }) => {
  4203. if (appendedCount >= maxAppends) return; // Stop if max appends reached
  4204.  
  4205. // Log each keyword being checked
  4206. console.log(`Checking keyword: ${entryKey}`);
  4207.  
  4208. // Check input element text for complete word match of keyword (case-insensitive)
  4209. const inputText = inputElement.value || inputElement.textContent;
  4210.  
  4211. // Combine check for keyword in input, in the last message, or in the mp3 link value
  4212. const isKeywordInInput = inputText && new RegExp(`\\b${entryKey}\\b`, 'i').test(inputText);
  4213. const isKeywordInLastMessage = lastMessage.some(message => {
  4214. const lastMessageText = message.textContent;
  4215. return new RegExp(`\\b${entryKey}\\b`, 'i').test(lastMessageText);
  4216. });
  4217. const isKeywordInMp3LinkValue = mp3LinkValue && mp3LinkValue.includes(entryKey);
  4218.  
  4219. console.log(`Keyword '${entryKey}' in input: ${isKeywordInInput}, in last message: ${isKeywordInLastMessage}, in MP3 value: ${isKeywordInMp3LinkValue}`);
  4220.  
  4221. if ((isKeywordInInput || isKeywordInLastMessage || isKeywordInMp3LinkValue) && entryValue) {
  4222. const keywordAlreadyUsed = relevantMessages.some(bracketContent => {
  4223. return new RegExp(`\\b${entryKey}\\b`, 'i').test(bracketContent);
  4224. });
  4225.  
  4226. if (!keywordAlreadyUsed) {
  4227. // Append the entryValue to the input element only if entryValue is not null or empty
  4228. if (inputElement.nodeName === 'TEXTAREA') {
  4229. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  4230. nativeInputValueSetter.call(inputElement, inputElement.value + '\n' + entryValue);
  4231.  
  4232. const inputEvent = new Event('input', {
  4233. bubbles: true,
  4234. cancelable: true,
  4235. });
  4236. inputElement.dispatchEvent(inputEvent);
  4237. } else {
  4238. const inputEvent = new InputEvent('beforeinput', {
  4239. bubbles: true,
  4240. cancelable: true,
  4241. inputType: 'insertText',
  4242. data: '\n' + entryValue,
  4243. });
  4244. inputElement.dispatchEvent(inputEvent);
  4245. }
  4246. appendedCount++; // Increment the count
  4247. console.log(`Keyword '${entryKey}' detected. Appended lorebook entry to the input.`);
  4248. } else {
  4249. console.log(`Keyword '${entryKey}' already found in recent bracketed messages or entryValue is null/empty. Skipping append.`);
  4250. }
  4251. }
  4252. });
  4253.  
  4254. // Log the total number of entries appended
  4255. console.log(`Total lorebook entries appended: ${appendedCount}`);
  4256. }
  4257. }
  4258.  
  4259.  
  4260.  
  4261.  
  4262. function getRandomEntry(inputElement) {
  4263. const selectedProfile = localStorage.getItem('selectedProfile.events');
  4264. if (selectedProfile) {
  4265. let profileEntries = [];
  4266. const currentHour = new Date().getHours();
  4267. for (let key in localStorage) {
  4268. if (key.startsWith(`${selectedProfile}.events:`)) {
  4269. const entryData = JSON.parse(localStorage.getItem(key));
  4270. const [startHour, endHour] = entryData.timeRange.split('-').map(Number);
  4271. // Check if current hour is within the time range
  4272. if (
  4273. (startHour <= endHour && currentHour >= startHour && currentHour < endHour) || // Normal range
  4274. (startHour > endHour && (currentHour >= startHour || currentHour < endHour)) // Crosses midnight
  4275. ) {
  4276. profileEntries.push({ key, ...entryData });
  4277. }
  4278. }
  4279. }
  4280.  
  4281. if (profileEntries.length > 0) {
  4282. const probability = parseInt(localStorage.getItem('events.probability') || '100', 10);
  4283. let selectedEntry = null;
  4284. while (profileEntries.length > 0) {
  4285. // Randomly select an entry from the available entries
  4286. const randomIndex = Math.floor(Math.random() * profileEntries.length);
  4287. const randomEntry = profileEntries[randomIndex];
  4288. // Check if the entry passes the individual probability check
  4289. if (Math.random() * 100 < randomEntry.probability) {
  4290. selectedEntry = randomEntry;
  4291. break;
  4292. } else {
  4293. // Remove the entry from the list if it fails the probability check
  4294. profileEntries.splice(randomIndex, 1);
  4295. }
  4296. }
  4297. if (selectedEntry && Math.random() * 100 < probability) {
  4298. console.log(`Random Entry Selected: ${selectedEntry.value}`);
  4299. appendToInput(inputElement, selectedEntry.value); // Append the random entry to the input element
  4300. }
  4301. } else {
  4302. console.log('No entries available for the selected profile in the current time range.');
  4303. }
  4304. } else {
  4305. console.log('No profile selected. Please select a profile to retrieve a random entry.');
  4306. }
  4307. }
  4308.  
  4309. // Helper function to append text to the input element
  4310. function appendToInput(inputElement, text) {
  4311. const lineBreak = '\n';
  4312. if (!inputElement) {
  4313. console.error('Input element not found.');
  4314. return;
  4315. }
  4316.  
  4317. if (inputElement.nodeName === 'TEXTAREA') {
  4318. // Mobile: Append text to <textarea>
  4319. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  4320. nativeInputValueSetter.call(inputElement, inputElement.value + `${lineBreak}${text}`);
  4321.  
  4322. const inputEvent = new Event('input', {
  4323. bubbles: true,
  4324. cancelable: true,
  4325. });
  4326. inputElement.dispatchEvent(inputEvent);
  4327. } else if (inputElement.hasAttribute('data-slate-editor')) {
  4328. // Desktop: Append text for Slate editor
  4329. const inputEvent = new InputEvent('beforeinput', {
  4330. bubbles: true,
  4331. cancelable: true,
  4332. inputType: 'insertText',
  4333. data: `${lineBreak}${text}`,
  4334. });
  4335. inputElement.dispatchEvent(inputEvent);
  4336. } else {
  4337. console.error('Unsupported input element type.');
  4338. }
  4339. }
  4340.  
  4341.  
  4342.  
  4343. // Expose the function to be accessible from other scripts
  4344. window.getRandomEntry = getRandomEntry;
  4345.  
  4346.  
  4347. }
  4348.  
  4349. }
  4350.  
  4351. // =================================================================== BUTTONS ==================================================================
  4352. function createDivider() {
  4353. const divider = document.createElement('div');
  4354. divider.style.height = '1px';
  4355. divider.style.backgroundColor = '#212121';
  4356. divider.style.margin = '20px 20px 0px 20px';
  4357. return divider;
  4358. }
  4359.  
  4360. function createFooterText() {
  4361. const footerText = document.createElement('div');
  4362. footerText.textContent = '© Vishanka 2024';
  4363. footerText.style.position = 'absolute';
  4364. footerText.style.bottom = '10px';
  4365. footerText.style.left = '50%';
  4366. footerText.style.transform = 'translateX(-50%)';
  4367. footerText.style.fontSize = '12px';
  4368. footerText.style.fontWeight = '550';
  4369. footerText.style.color = '#272727';
  4370. return footerText;
  4371. }
  4372.  
  4373. function initializeButton() {
  4374. DCstoragePanel.appendChild(openLorebookButton);
  4375.  
  4376.  
  4377. DCstoragePanel.appendChild(eventsButton);
  4378.  
  4379.  
  4380. DCstoragePanel.appendChild(manageRulesButton);
  4381. DCstoragePanel.appendChild(notifierToggleButton);
  4382. DCstoragePanel.appendChild(createDivider()); // Add divider after manageRulesButton
  4383.  
  4384. DCstoragePanel.appendChild(serverbartoggleButton);
  4385.  
  4386.  
  4387. DCstoragePanel.appendChild(mp3ToggleButton);
  4388. DCstoragePanel.appendChild(createDivider()); // Add divider after mp3ToggleButton
  4389.  
  4390. DCstoragePanel.appendChild(exportButton);
  4391.  
  4392. DCstoragePanel.appendChild(importButton);
  4393. DCstoragePanel.appendChild(createDivider()); // Add divider after importButton
  4394.  
  4395. DCstoragePanel.appendChild(settingsWindow);
  4396.  
  4397. DCstoragePanel.appendChild(createFooterText());
  4398. }
  4399.  
  4400. // ============================================================ SCRIPT LOADING ORDER ============================================================
  4401.  
  4402. LorebookScript();
  4403. NotifierScript();
  4404. ImportExportScript();
  4405. EventsScript();
  4406. RulesScript();
  4407. mp3Script();
  4408. StylesScript();
  4409. HiderScript();
  4410. MainLogicScript();
  4411. initializeButton();
  4412.  
  4413.  
  4414. })();