Translate Selection Sidebar and Tooltip

Translate selected text, show in a tooltip, add to a sidebar list, and store in local storage

目前为 2024-08-10 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Translate Selection Sidebar and Tooltip
  3. // @namespace https://aveusaid.wordpress.com
  4. // @version 0.9082024
  5. // @description Translate selected text, show in a tooltip, add to a sidebar list, and store in local storage
  6. // @author Usaid Bin Khalid Khan
  7. // @match *://*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Add CSS for the tooltip, sidebar, buttons, and dark mode text
  16. const styleElement = document.createElement('style');
  17. styleElement.type = 'text/css';
  18. styleElement.innerHTML = `
  19. .translator-tooltip {
  20. font-weight: 700;
  21. color: #000000;
  22. position: absolute;
  23. z-index: 10000;
  24. padding: 8px 12px;
  25. max-width: 300px;
  26. border-radius: 0.3em;
  27. background-color: #ffffdb;
  28. border: 1px solid #ccc;
  29. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  30. text-align: center;
  31. font-size: 18px;
  32. line-height: 1.4;
  33. visibility: hidden;
  34. opacity: 0;
  35. transition: visibility 0s linear 300ms, opacity 300ms;
  36. }
  37.  
  38. .translator-tooltip.visible {
  39. visibility: visible;
  40. opacity: 1;
  41. }
  42.  
  43. .translator-sidebar {
  44. position: fixed;
  45. top: 0;
  46. right: 0;
  47. width: 280px;
  48. height: 100%;
  49. background-color: #f7f7f7;
  50. overflow-y: auto;
  51. border-left: 2px solid #3388CC;
  52. padding: 20px;
  53. z-index: 10000;
  54. font-size: 16px;
  55. overflow: auto;
  56. box-shadow: -2px 0 5px rgba(0,0,0,0.1);
  57. box-sizing: border-box;
  58. resize: horizontal;
  59. min-width: 200px;
  60. max-width: 500px;
  61. display: none;
  62. }
  63.  
  64. .translator-sidebar > * {
  65. margin-bottom: 20px;
  66. }
  67.  
  68. .translator-entry {
  69. display: flex;
  70. flex-direction: column;
  71. padding: 10px 0;
  72. margin-bottom: 10px;
  73. border-bottom: 1px solid #ccc;
  74. }
  75.  
  76. .translator-entry span:first-child {
  77. margin-bottom: 6px;
  78. font-weight: bold;
  79. color: #333;
  80. }
  81.  
  82. .translator-entry span:last-child {
  83. color: #666;
  84. }
  85.  
  86. .close-sidebar {
  87. position: absolute;
  88. top: 10px;
  89. right: 10px;
  90. cursor: pointer;
  91. font-weight: bold;
  92. color: #555;
  93. font-size: 20px;
  94. background-color: transparent;
  95. border: none;
  96. z-index: 10001;
  97. }
  98.  
  99. .attractive-text {
  100. display: block;
  101. font-size: 14px;
  102. font-style: italic;
  103. text-decoration: none;
  104. color: #ff6666;
  105. margin-top: 20px;
  106. text-align: center;
  107. }
  108.  
  109. .attractive-text:hover {
  110. color: #ff3333;
  111. }
  112.  
  113. .clear-button,
  114. .copy-all-button,
  115. .mode-toggle {
  116. cursor: pointer;
  117. padding: 10px 15px;
  118. background-color: #f5f5f5;
  119. border: 1px solid #ccc;
  120. border-radius: 5px;
  121. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  122. font-size: 16px;
  123. margin: 10px 0;
  124. width: calc(100% - 20px);
  125. box-sizing: border-box;
  126. }
  127.  
  128. .clear-button {
  129. margin-top: 20px;
  130. }
  131.  
  132. .mode-toggle {
  133. display: flex;
  134. align-items: center;
  135. margin-top: 20px;
  136. }
  137.  
  138. .mode-toggle span {
  139. margin-right: 10px;
  140. }
  141.  
  142. .close-sidebar {
  143. position: absolute;
  144. top: 10px;
  145. right: 10px;
  146. cursor: pointer;
  147. font-weight: bold;
  148. color: #555;
  149. font-size: 20px;
  150. background-color: transparent;
  151. border: none;
  152. z-index: 10001;
  153. }
  154.  
  155. .attractive-text {
  156. display: block;
  157. font-size: 14px;
  158. font-style: italic;
  159. text-decoration: none;
  160. color: #ff6666;
  161. margin-top: 20px;
  162. text-align: center;
  163. }
  164.  
  165. .attractive-text:hover {
  166. color: #ff3333;
  167. }
  168.  
  169. .error-message {
  170. position: fixed;
  171. top: 50%;
  172. left: 50%;
  173. transform: translate(-50%, -50%);
  174. padding: 20px;
  175. background-color: #f8d7da;
  176. color: #721c24;
  177. border: 1px solid #f5c6cb;
  178. border-radius: 5px;
  179. z-index: 10002;
  180. font-size: 18px;
  181. text-align: center;
  182. visibility: hidden;
  183. opacity: 0;
  184. transition: visibility 0s linear 300ms, opacity 300ms;
  185. }
  186.  
  187. .error-message.visible {
  188. visibility: visible;
  189. opacity: 1;
  190. }
  191.  
  192. .open-sidebar-button {
  193. position: fixed;
  194. bottom: 140px;
  195. right: 20px;
  196. z-index: 10001;
  197. cursor: pointer;
  198. padding: 10px 15px;
  199. background-color: #f5f5f5;
  200. border: 1px solid #ccc;
  201. border-radius: 5px;
  202. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  203. font-size: 16px;
  204. display: block;
  205. }
  206.  
  207. .empty-sidebar {
  208. font-style: italic;
  209. color: grey;
  210. margin-top: 20px;
  211. text-align: center;
  212. }
  213.  
  214. .dark-mode .translator-sidebar {
  215. background-color: #333333; /* Dark background color */
  216. color: #ffffff; /* White text */
  217. }
  218.  
  219. .dark-mode .translator-entry span:first-child {
  220. color: #FFD700; /* Sunset yellow */
  221. }
  222.  
  223. .dark-mode .translator-entry span:last-child {
  224. color: #ffffff; /* White */
  225. }
  226.  
  227. .dark-mode .clear-button,
  228. .dark-mode .open-sidebar-button,
  229. .dark-mode .copy-all-button {
  230. background-color: #444;
  231. color: #FFD700; /* Sunset yellow */
  232. border-color: #666;
  233. }
  234. `;
  235. document.head.appendChild(styleElement);
  236.  
  237. // Create tooltip element
  238. const tooltip = document.createElement('div');
  239. tooltip.className = 'translator-tooltip';
  240. document.body.appendChild(tooltip);
  241.  
  242. // Create sidebar element
  243. const sidebar = document.createElement('div');
  244. sidebar.className = 'translator-sidebar resizable';
  245. document.body.appendChild(sidebar);
  246.  
  247. // Create close button for sidebar
  248. const closeButton = document.createElement('button');
  249. closeButton.innerHTML = '×';
  250. closeButton.className = 'close-sidebar';
  251. closeButton.setAttribute('aria-label', 'Close sidebar');
  252. sidebar.appendChild(closeButton);
  253.  
  254. // Create "Cogito, Ergo Sum" text
  255. const attractText = document.createElement('a');
  256. attractText.innerHTML = 'Cogito, Ergo Sum';
  257. attractText.href = 'https://aveusaid.wordpress.com';
  258. attractText.className = 'attractive-text';
  259. attractText.setAttribute('role', 'link');
  260. attractText.setAttribute('aria-label', 'Cogito, Ergo Sum');
  261. sidebar.appendChild(attractText);
  262.  
  263. // Add two italic lines for empty sidebar
  264. const emptyLines = document.createElement('div');
  265. emptyLines.className = 'empty-sidebar';
  266. emptyLines.innerHTML = `
  267. <i>The Archives are empty.</i> <br>
  268. <i>Select some text to add it to the Translation Archives.</i>
  269. `;
  270. sidebar.appendChild(emptyLines);
  271.  
  272. // Create clear button
  273. const clearButton = document.createElement('button');
  274. clearButton.textContent = 'Tabula Rasa';
  275. clearButton.className = 'clear-button';
  276. clearButton.setAttribute('aria-label', 'Clear translations');
  277. sidebar.appendChild(clearButton);
  278.  
  279. // Create copy all button
  280. const copyAllButton = document.createElement('button');
  281. copyAllButton.textContent = 'Copy All';
  282. copyAllButton.className = 'copy-all-button';
  283. copyAllButton.setAttribute('aria-label', 'Copy all translations');
  284. sidebar.appendChild(copyAllButton);
  285.  
  286. // Create mode toggle button
  287. const modeToggleButton = document.createElement('button');
  288. modeToggleButton.className = 'mode-toggle';
  289. modeToggleButton.textContent = 'Dim the Lights';
  290. sidebar.appendChild(modeToggleButton);
  291.  
  292. // Create open sidebar button
  293. const openSidebarButton = document.createElement('button');
  294. openSidebarButton.textContent = 'The Archive';
  295. openSidebarButton.className = 'open-sidebar-button';
  296. openSidebarButton.setAttribute('aria-label', 'Open translation archive');
  297. document.body.appendChild(openSidebarButton);
  298.  
  299. // Error message element
  300. const errorMessage = document.createElement('div');
  301. errorMessage.className = 'error-message';
  302. document.body.appendChild(errorMessage);
  303.  
  304. let translations = [];
  305.  
  306. function showError(message) {
  307. errorMessage.textContent = message;
  308. errorMessage.classList.add('visible');
  309. setTimeout(() => {
  310. errorMessage.classList.remove('visible');
  311. }, 3000);
  312. }
  313.  
  314. function translateText(text) {
  315. return fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=${encodeURIComponent(text)}`)
  316. .then(response => response.json())
  317. .then(result => result[0][0][0])
  318. .catch(error => {
  319. showError('ERROR: The Network Towers Have Fallen!');
  320. console.error(error);
  321. });
  322. }
  323.  
  324. function updateSidebar() {
  325. sidebar.innerHTML = '';
  326. sidebar.appendChild(closeButton);
  327. sidebar.appendChild(attractText);
  328. if (translations.length === 0) {
  329. sidebar.appendChild(emptyLines);
  330. } else {
  331. translations.forEach(({ original, translated }) => {
  332. const entry = document.createElement('div');
  333. entry.className = 'translator-entry';
  334. entry.innerHTML = `
  335. <span>${original}</span>
  336. <span>${translated}</span>
  337. `;
  338. sidebar.appendChild(entry);
  339. });
  340. }
  341. sidebar.appendChild(clearButton);
  342. sidebar.appendChild(copyAllButton);
  343. sidebar.appendChild(modeToggleButton);
  344. }
  345.  
  346. function toggleSidebar() {
  347. if (sidebar.style.display === 'block') {
  348. sidebar.style.display = 'none';
  349. openSidebarButton.style.display = 'block'; // Show "The Archive" button
  350. } else {
  351. updateSidebar();
  352. sidebar.style.display = 'block';
  353. openSidebarButton.style.display = 'none'; // Hide "The Archive" button
  354. }
  355. }
  356.  
  357. function clearTranslations() {
  358. translations = [];
  359. updateSidebar();
  360. }
  361.  
  362. function copyAllTranslations() {
  363. const allTranslations = translations.map(({ original, translated }) => `${original}: ${translated}`).join('\n');
  364. navigator.clipboard.writeText(allTranslations).then(() => {
  365. showError('Translations copied to clipboard!');
  366. }).catch(() => {
  367. showError('Failed to copy translations.');
  368. });
  369. }
  370.  
  371. function toggleDarkMode() {
  372. document.body.classList.toggle('dark-mode');
  373. const isDarkMode = document.body.classList.contains('dark-mode');
  374. modeToggleButton.textContent = isDarkMode ? 'Light the Way' : 'Dim the Lights';
  375. }
  376.  
  377. document.addEventListener('mouseup', async (e) => {
  378. if (window.getSelection().toString()) {
  379. const selectedText = window.getSelection().toString();
  380. const translatedText = await translateText(selectedText);
  381. translations.push({ original: selectedText, translated: translatedText });
  382. updateSidebar();
  383. tooltip.textContent = translatedText;
  384. tooltip.style.left = `${e.pageX}px`;
  385. tooltip.style.top = `${e.pageY + 10}px`;
  386. tooltip.classList.add('visible');
  387. setTimeout(() => {
  388. tooltip.classList.remove('visible');
  389. }, 3000);
  390. }
  391. });
  392.  
  393. openSidebarButton.addEventListener('click', toggleSidebar);
  394. closeButton.addEventListener('click', toggleSidebar);
  395. clearButton.addEventListener('click', clearTranslations);
  396. copyAllButton.addEventListener('click', copyAllTranslations);
  397. modeToggleButton.addEventListener('click', toggleDarkMode);
  398.  
  399. })();