Copy Crushon Story

Copies all listed chat bubbles with formatting

  1. // ==UserScript==
  2. // @name Copy Crushon Story
  3. // @namespace https://crushon.ai/
  4. // @version 0.2
  5. // @description Copies all listed chat bubbles with formatting
  6. // @author dbzfanatic
  7. // @match *://crushon.ai/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=crushon.ai
  9. // @grant GM_setClipboard
  10. // @license GPLv3
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Create the custom context menu
  17. const menu = document.createElement('ul');
  18. menu.id = 'custom-menu';
  19. menu.style.display = 'none';
  20. menu.style.position = 'absolute';
  21. menu.style.backgroundColor = 'black';
  22. menu.style.border = '1px solid #ccc';
  23. menu.style.zIndex = '1000';
  24. menu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
  25. document.body.appendChild(menu);
  26.  
  27. const copyStoryMenuItem = document.createElement('li');
  28. copyStoryMenuItem.id = 'copy-story';
  29. copyStoryMenuItem.textContent = 'Copy Story';
  30. copyStoryMenuItem.style.padding = '5px';
  31. copyStoryMenuItem.style.cursor = 'pointer';
  32. copyStoryMenuItem.style.color = 'white';
  33. copyStoryMenuItem.style.listStyleType = 'none';
  34. copyStoryMenuItem.onmouseover = function() {
  35. copyStoryMenuItem.style.backgroundColor = '#222';
  36. };
  37. copyStoryMenuItem.onmouseout = function() {
  38. copyStoryMenuItem.style.backgroundColor = 'black';
  39. };
  40. menu.appendChild(copyStoryMenuItem);
  41.  
  42. const copyAsPlainMenuItem = document.createElement('li');
  43. copyAsPlainMenuItem.id = 'copy-as-plain';
  44. copyAsPlainMenuItem.textContent = 'Copy as Plain';
  45. copyAsPlainMenuItem.style.padding = '5px';
  46. copyAsPlainMenuItem.style.cursor = 'pointer';
  47. copyAsPlainMenuItem.style.color = 'white';
  48. copyAsPlainMenuItem.style.listStyleType = 'none';
  49. copyAsPlainMenuItem.onmouseover = function() {
  50. copyAsPlainMenuItem.style.backgroundColor = '#222';
  51. };
  52. copyAsPlainMenuItem.onmouseout = function() {
  53. copyAsPlainMenuItem.style.backgroundColor = 'black';
  54. };
  55. menu.appendChild(copyAsPlainMenuItem);
  56.  
  57. const copyAsHTMLMenuItem = document.createElement('li');
  58. copyAsHTMLMenuItem.id = 'copy-as-html';
  59. copyAsHTMLMenuItem.textContent = 'Copy as HTML';
  60. copyAsHTMLMenuItem.style.padding = '5px';
  61. copyAsHTMLMenuItem.style.cursor = 'pointer';
  62. copyAsHTMLMenuItem.style.color = 'white';
  63. copyAsHTMLMenuItem.style.listStyleType = 'none';
  64. copyAsHTMLMenuItem.onmouseover = function() {
  65. copyAsHTMLMenuItem.style.backgroundColor = '#222';
  66. };
  67. copyAsHTMLMenuItem.onmouseout = function() {
  68. copyAsHTMLMenuItem.style.backgroundColor = 'black';
  69. };
  70. menu.appendChild(copyAsHTMLMenuItem);
  71.  
  72. const copyForTelegramMenuItem = document.createElement('li');
  73. copyForTelegramMenuItem.id = 'copy-for-telegram';
  74. copyForTelegramMenuItem.textContent = 'Copy for Telegram';
  75. copyForTelegramMenuItem.style.padding = '5px';
  76. copyForTelegramMenuItem.style.cursor = 'pointer';
  77. copyForTelegramMenuItem.style.color = 'white';
  78. copyForTelegramMenuItem.style.listStyleType = 'none';
  79. copyForTelegramMenuItem.onmouseover = function() {
  80. copyForTelegramMenuItem.style.backgroundColor = '#222';
  81. };
  82. copyForTelegramMenuItem.onmouseout = function() {
  83. copyForTelegramMenuItem.style.backgroundColor = 'black';
  84. };
  85. menu.appendChild(copyForTelegramMenuItem);
  86.  
  87. // Function to check if the event target is inside an excluded div
  88. function isInsideExcludedDiv(target) {
  89. return target.closest('.flex.flex-col.gap-3.self-stretch, .flex.w-full.items-start.justify-between.gap-4') !== null;
  90. }
  91.  
  92. // Show the custom context menu on right-click, unless inside the excluded div
  93. document.addEventListener('contextmenu', function(event) {
  94. if (isInsideExcludedDiv(event.target)) {
  95. menu.style.display = 'none';
  96. return; // Do nothing if inside the excluded div
  97. }
  98.  
  99. event.preventDefault();
  100. menu.style.top = `${event.pageY}px`;
  101. menu.style.left = `${event.pageX}px`;
  102. menu.style.display = 'block';
  103. });
  104.  
  105. // Hide the custom menu on click elsewhere
  106. document.addEventListener('click', function() {
  107. menu.style.display = 'none';
  108. });
  109.  
  110. // Function to extract text between <span> and </span>
  111. function extractSpanText(html) {
  112. const spanMatches = html.match(/<span[^>]*>(.*?)<\/span>/g);
  113. if (spanMatches) {
  114. return spanMatches.map(span => span.replace(/<\/?span[^>]*>/g, ''));
  115. }
  116. return [];
  117. }
  118.  
  119. // Function to convert HTML to rich text with text formatting only
  120. function htmlToRichText(html) {
  121. // Replace <strong> with bold formatting
  122. html = html.replace(/<strong>/g, '<b>');
  123. html = html.replace(/<\/strong>/g, '</b>');
  124. // Replace <em> with italic formatting
  125. html = html.replace(/<em>/g, '<i>');
  126. html = html.replace(/<\/em>/g, '</i>');
  127. // Add more replacements as needed for other HTML tags
  128.  
  129. // Create a temporary div element
  130. const tempDiv = document.createElement('div');
  131. // Set its innerHTML
  132. tempDiv.innerHTML = html;
  133.  
  134. // Strip any unwanted styles
  135. tempDiv.querySelectorAll('*').forEach(node => {
  136. node.removeAttribute('style'); // Remove all inline styles
  137. node.removeAttribute('bgcolor'); // Remove background color attribute
  138. // Add more style removals as necessary
  139. });
  140.  
  141. // Return the innerText of the temporary div
  142. return tempDiv.innerHTML;
  143. }
  144.  
  145. // Function to handle copying story as rich text
  146. function copyStory() {
  147. // Get all divs with the specified class
  148. const charDivs = document.querySelectorAll('.flex.flex-1.flex-col.items-start.gap-2');
  149. // Create a string array containing the innerHTML of each div
  150. const charDivContents = Array.from(charDivs).map(div => div.innerHTML);
  151.  
  152. // Extract span contents from charDivContents
  153. const extractedCharContents = charDivContents.flatMap(html => extractSpanText(html));
  154.  
  155. // Get all divs with the specified class
  156. const markdownDivs = document.querySelectorAll('.not-prose.w-full.MarkdownText_CustomMarkdownText__P3bB6');
  157. // Create a string array containing the innerHTML of each div
  158. const markdownContents = Array.from(markdownDivs).map(div => div.innerHTML);
  159.  
  160. // Create a string to store the final story
  161. let storyString = '';
  162.  
  163. // Iterate through the indices of markdownContents
  164. for (let i = 0; i < markdownContents.length; i++) {
  165. // Convert HTML to rich text
  166. let richTextContent = htmlToRichText(markdownContents[i]);
  167.  
  168. if (i % 2 === 0 && extractedCharContents.length > 0) {
  169. storyString += "\n" + extractedCharContents[0] + ": \n" + richTextContent + "\n";
  170. } else {
  171. storyString += "\n{{user}}: \n" + richTextContent + "\n";
  172. }
  173. }
  174.  
  175. // Create a temporary element to hold the HTML
  176. const tempCopyDiv = document.createElement('div');
  177.  
  178. tempCopyDiv.innerHTML = storyString;
  179. document.body.appendChild(tempCopyDiv);
  180.  
  181. // Create a range and selection to select the contents of the tempCopyDiv
  182. const range = document.createRange();
  183. range.selectNodeContents(tempCopyDiv);
  184. const selection = window.getSelection();
  185. selection.removeAllRanges();
  186. selection.addRange(range);
  187.  
  188. // Copy the selection to the clipboard
  189. try {
  190. document.execCommand('copy');
  191. } catch (err) {
  192. console.error('Unable to copy to clipboard', err);
  193. }
  194.  
  195. // Clean up the selection and remove the temporary element
  196. selection.removeAllRanges();
  197. document.body.removeChild(tempCopyDiv);
  198.  
  199. // Hide the custom menu after click
  200. menu.style.display = 'none';
  201. }
  202.  
  203.  
  204.  
  205. // Function to handle copying story as plain text for Telegram
  206. function copyAsPlain() {
  207. // Get all divs with the specified class
  208. const charDivs = document.querySelectorAll('.flex.flex-1.flex-col.items-start.gap-2');
  209. // Create a string array containing the innerHTML of each div
  210. const charDivContents = Array.from(charDivs).map(div => div.innerHTML);
  211.  
  212. // Extract span contents from charDivContents
  213. const extractedCharContents = charDivContents.flatMap(html => extractSpanText(html));
  214.  
  215. // Get all divs with the specified class
  216. const markdownDivs = document.querySelectorAll('.not-prose.w-full.MarkdownText_CustomMarkdownText__P3bB6');
  217. // Create a string array containing the innerHTML of each div
  218. const markdownContents = Array.from(markdownDivs).map(div => div.innerHTML);
  219.  
  220. // Create a string to store the final story
  221. let storyString = '';
  222.  
  223. // Iterate through the indices of markdownContents
  224. for (let i = 0; i < markdownContents.length; i++) {
  225. // Convert HTML to plain text
  226. let plainTextContent = htmlToPlainText(markdownContents[i]);
  227.  
  228. if (i % 2 === 0 && extractedCharContents.length > 0) {
  229. storyString += "\n" + extractedCharContents[0] + ": \n" + plainTextContent + "\n";
  230. } else {
  231. storyString += "\n{{user}}: \n" + plainTextContent + "\n";
  232. }
  233. }
  234.  
  235. // Copy storyString to clipboard as plain text for Telegram
  236. GM_setClipboard(storyString, 'text/plain');
  237.  
  238. // Hide the custom menu after click
  239. menu.style.display = 'none';
  240. }
  241.  
  242. // Function to handle copying for Telegram
  243. function copyForTelegram() {
  244. // Get all divs with the specified class
  245. const charDivs = document.querySelectorAll('.flex.flex-1.flex-col.items-start.gap-2');
  246. // Create a string array containing the innerHTML of each div
  247. const charDivContents = Array.from(charDivs).map(div => div.innerHTML);
  248.  
  249. // Extract span contents from charDivContents
  250. const extractedCharContents = charDivContents.flatMap(html => extractSpanText(html));
  251.  
  252. // Get all divs with the specified class
  253. const markdownDivs = document.querySelectorAll('.not-prose.w-full.MarkdownText_CustomMarkdownText__P3bB6');
  254. // Create a string array containing the innerHTML of each div
  255. const markdownContents = Array.from(markdownDivs).map(div => div.innerHTML);
  256.  
  257. // Create a string to store the final story
  258. let storyString = '';
  259.  
  260. // Iterate through the indices of markdownContents
  261. for (let i = 0; i < markdownContents.length; i++) {
  262. // Convert HTML to rich text
  263. let richTextContent = htmlToTelegram(markdownContents[i]);
  264.  
  265. if (i % 2 === 0 && extractedCharContents.length > 0) {
  266. storyString += "\n" + extractedCharContents[0] + ": " + richTextContent + "\n";
  267. } else {
  268. storyString += "\n{{user}}: " + richTextContent + "\n";
  269. }
  270. }
  271.  
  272. // Copy storyString to clipboard as plain text for Telegram
  273. GM_setClipboard(storyString, 'text/plain');
  274. }
  275.  
  276. // Function to handle copying for Telegram
  277. function copyWithHtml() {
  278. // Get all divs with the specified class
  279. const charDivs = document.querySelectorAll('.flex.flex-1.flex-col.items-start.gap-2');
  280. // Create a string array containing the innerHTML of each div
  281. const charDivContents = Array.from(charDivs).map(div => div.innerHTML);
  282.  
  283. // Extract span contents from charDivContents
  284. const extractedCharContents = charDivContents.flatMap(html => extractSpanText(html));
  285.  
  286. // Get all divs with the specified class
  287. const markdownDivs = document.querySelectorAll('.not-prose.w-full.MarkdownText_CustomMarkdownText__P3bB6');
  288. // Create a string array containing the innerHTML of each div
  289. const markdownContents = Array.from(markdownDivs).map(div => div.innerHTML);
  290.  
  291. // Create a string to store the final story
  292. let storyString = '';
  293.  
  294. // Iterate through the indices of markdownContents
  295. for (let i = 0; i < markdownContents.length; i++) {
  296. // Convert HTML to rich text
  297. let richTextContent = htmlToRichText(markdownContents[i]);
  298.  
  299. if (i % 2 === 0 && extractedCharContents.length > 0) {
  300. storyString += "\n" + extractedCharContents[0] + ": \n" + richTextContent + "\n";
  301. } else {
  302. storyString += "\n{{user}}: \n" + richTextContent + "\n";
  303. }
  304. }
  305.  
  306. // Copy storyString to clipboard as plain text for Telegram
  307. GM_setClipboard(storyString, 'text/plain');
  308. }
  309.  
  310. // Function to convert HTML to plain text
  311. function htmlToPlainText(html) {
  312. // Create a temporary div element
  313. const tempDiv = document.createElement('div');
  314. // Set its innerHTML
  315. tempDiv.innerHTML = html;
  316.  
  317. // Return the innerText of the temporary div
  318. return tempDiv.innerText;
  319. }
  320.  
  321. // Function to convert HTML to rich text with text formatting only
  322. function htmlToTelegram(html) {
  323. // Replace <strong> with bold formatting
  324. html = html.replace(/(<strong.*>)\b/g, '**');
  325. html = html.replace(/<\/strong>/g, '**');
  326. // Replace <em> with italic formatting
  327. html = html.replace(/(<em.*>)\b/g, '__');
  328. html = html.replace(/<\/em>/g, '__');
  329. // Replace <p> with new line
  330. html = html.replace(/<p>/g, '\n');
  331. html = html.replace(/<\/p>/g, '');
  332.  
  333. // Create a temporary div element
  334. const tempDiv = document.createElement('div');
  335. // Set its innerHTML
  336. tempDiv.innerHTML = html;
  337.  
  338. // Strip any unwanted styles
  339. tempDiv.querySelectorAll('*').forEach(node => {
  340. node.removeAttribute('style'); // Remove all inline styles
  341. node.removeAttribute('bgcolor'); // Remove background color attribute
  342. // Add more style removals as necessary
  343. });
  344.  
  345. // Return the innerText of the temporary div
  346. return tempDiv.innerText;
  347. }
  348.  
  349. // Add the click event listener to the "Copy Story" item
  350. document.getElementById('copy-story').addEventListener('click', function() {
  351. copyStory();
  352. });
  353.  
  354. // Add the click event listener to the "Copy as HTML" item
  355. document.getElementById('copy-as-html').addEventListener('click', function() {
  356. copyWithHtml();
  357. });
  358.  
  359. // Add the click event listener to the "Copy as Plain" item
  360. document.getElementById('copy-as-plain').addEventListener('click', function() {
  361. copyAsPlain();
  362. });
  363.  
  364. // Add the click event listener to the "Copy for Telegram" item
  365. document.getElementById('copy-for-telegram').addEventListener('click', function() {
  366. copyForTelegram();
  367. });
  368. })();