content.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. (function() {
  2. let playerContainer = null;
  3. function createPlayer() {
  4. if (playerContainer) return;
  5. playerContainer = document.createElement('div');
  6. playerContainer.id = 'readvox-player';
  7. const playerIframe = document.createElement('iframe');
  8. playerIframe.src = chrome.runtime.getURL('popup.html');
  9. playerIframe.className = 'player-iframe';
  10. playerContainer.appendChild(playerIframe);
  11. document.body.appendChild(playerContainer);
  12. makeDraggable(playerContainer);
  13. }
  14. function removePlayer() {
  15. if (playerContainer) {
  16. playerContainer.remove();
  17. playerContainer = null;
  18. }
  19. }
  20. function makeDraggable(element) {
  21. let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  22. const dragMouseDown = (e) => {
  23. e = e || window.event;
  24. pos3 = e.clientX;
  25. pos4 = e.clientY;
  26. document.onmouseup = closeDragElement;
  27. document.onmousemove = elementDrag;
  28. };
  29. const elementDrag = (e) => {
  30. e = e || window.event;
  31. e.preventDefault();
  32. pos1 = pos3 - e.clientX;
  33. pos2 = pos4 - e.clientY;
  34. pos3 = e.clientX;
  35. pos4 = e.clientY;
  36. element.style.top = (element.offsetTop - pos2) + "px";
  37. element.style.left = (element.offsetLeft - pos1) + "px";
  38. };
  39. const closeDragElement = () => {
  40. document.onmouseup = null;
  41. document.onmousemove = null;
  42. };
  43. element.addEventListener('mousedown', dragMouseDown);
  44. }
  45. // ---------- Grammar Highlight Logic ----------
  46. const PARAGRAPH_SELECTORS = 'p, article p, section p, div';
  47. function callBackend(payload) {
  48. return new Promise((resolve, reject) => {
  49. chrome.runtime.sendMessage(
  50. { type: 'GRAMMAR_API_REQUEST', ...payload },
  51. (response) => {
  52. if (chrome.runtime.lastError) {
  53. reject(new Error(chrome.runtime.lastError.message));
  54. return;
  55. }
  56. if (!response) {
  57. reject(new Error('后台未返回数据。'));
  58. return;
  59. }
  60. if (response.error) {
  61. reject(new Error(response.error));
  62. return;
  63. }
  64. resolve(response);
  65. }
  66. );
  67. });
  68. }
  69. function isParagraphElement(node) {
  70. return (
  71. node &&
  72. node.nodeType === Node.ELEMENT_NODE &&
  73. node.matches &&
  74. node.matches(PARAGRAPH_SELECTORS)
  75. );
  76. }
  77. function collectParagraphNodes(container) {
  78. if (!container || container.nodeType !== Node.ELEMENT_NODE) return [];
  79. const paragraphs = Array.from(container.querySelectorAll('p')).filter(
  80. (el) => el.innerText.trim()
  81. );
  82. if (paragraphs.length > 0) return paragraphs;
  83. return [container];
  84. }
  85. function buildParagraphTarget(nodes) {
  86. const usableNodes = nodes.filter(
  87. (node) => node && node.innerText && node.innerText.trim()
  88. );
  89. if (usableNodes.length === 0) {
  90. throw new Error('未找到可用段落文本。');
  91. }
  92. const originals = usableNodes.map((node) => node.innerHTML);
  93. const paragraphs = usableNodes.map((node) => node.innerText.trim());
  94. return {
  95. payload: { paragraphs },
  96. apply(htmlList) {
  97. const list = Array.isArray(htmlList) ? htmlList : [];
  98. usableNodes.forEach((node, idx) => {
  99. const html = list[idx];
  100. if (typeof html === 'string' && html.trim()) {
  101. node.innerHTML = html;
  102. }
  103. });
  104. },
  105. restore() {
  106. usableNodes.forEach((node, idx) => {
  107. node.innerHTML = originals[idx];
  108. });
  109. }
  110. };
  111. }
  112. function getSelectionTarget() {
  113. const selection = window.getSelection();
  114. if (!selection || selection.rangeCount === 0) {
  115. throw new Error('请先在页面中选择一段文本。');
  116. }
  117. const range = selection.getRangeAt(0);
  118. if (range.collapsed) {
  119. throw new Error('所选文本为空。');
  120. }
  121. const root =
  122. range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
  123. ? range.commonAncestorContainer
  124. : range.commonAncestorContainer.parentElement || document.body;
  125. const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
  126. acceptNode(node) {
  127. if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP;
  128. return isParagraphElement(node) && range.intersectsNode(node)
  129. ? NodeFilter.FILTER_ACCEPT
  130. : NodeFilter.FILTER_SKIP;
  131. }
  132. });
  133. const nodes = [];
  134. let current = walker.nextNode();
  135. while (current) {
  136. nodes.push(current);
  137. current = walker.nextNode();
  138. }
  139. if (nodes.length === 0) {
  140. const fallback =
  141. (range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
  142. ? range.commonAncestorContainer
  143. : range.commonAncestorContainer.parentElement) || document.body;
  144. nodes.push(fallback.closest(PARAGRAPH_SELECTORS) || fallback);
  145. }
  146. return buildParagraphTarget(nodes);
  147. }
  148. function getParagraphTarget() {
  149. const selection = window.getSelection();
  150. let node =
  151. selection && selection.rangeCount > 0
  152. ? selection.getRangeAt(0).commonAncestorContainer
  153. : document.activeElement;
  154. if (!node) {
  155. node = document.body;
  156. }
  157. if (node.nodeType === Node.TEXT_NODE) {
  158. node = node.parentElement;
  159. }
  160. const paragraph =
  161. node.closest(PARAGRAPH_SELECTORS) || document.querySelector('p') || document.body;
  162. return buildParagraphTarget([paragraph]);
  163. }
  164. function getArticleTarget() {
  165. const container =
  166. document.querySelector('article') ||
  167. document.querySelector('main') ||
  168. document.body;
  169. const nodes = collectParagraphNodes(container).filter(
  170. (node) => node.innerText.trim()
  171. );
  172. return buildParagraphTarget(nodes);
  173. }
  174. async function handleAnalyze(mode) {
  175. let targetGetter;
  176. switch (mode) {
  177. case 'selection':
  178. targetGetter = getSelectionTarget;
  179. break;
  180. case 'paragraph':
  181. targetGetter = getParagraphTarget;
  182. break;
  183. case 'page':
  184. case 'article':
  185. targetGetter = getArticleTarget;
  186. break;
  187. default:
  188. throw new Error('未知的分析模式。');
  189. }
  190. const target = targetGetter();
  191. try {
  192. const response = await callBackend(target.payload);
  193. const htmlList =
  194. response.highlightedHtmls ||
  195. (response.highlightedHtml ? [response.highlightedHtml] : []);
  196. if (!htmlList || htmlList.length === 0) {
  197. throw new Error('后台未返回数据。');
  198. }
  199. target.apply(htmlList);
  200. return {};
  201. } catch (error) {
  202. target.restore?.();
  203. throw error;
  204. }
  205. }
  206. // ---------- Message Handler ----------
  207. chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  208. if (request.action === "activatePlayer") {
  209. createPlayer();
  210. } else if (request.action === "deactivatePlayer") {
  211. removePlayer();
  212. } else if (request.type === 'EXECUTE_GRAMMAR') {
  213. handleAnalyze(request.mode)
  214. .then(() => sendResponse({ success: true }))
  215. .catch((error) => {
  216. sendResponse({ error: error.message || '未知错误' });
  217. });
  218. return true;
  219. }
  220. });
  221. })();