content.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. /**
  2. * 侧边栏管理类
  3. */
  4. class SidebarManager {
  5. /**
  6. * @constant {Object} 配置常量
  7. */
  8. static CONFIG = {
  9. SIDEBAR_ID: "paiwise-sidebar",
  10. WRAPPER_ID: "paiwise-main-wrapper",
  11. STORAGE_KEY: "sidebarOpen",
  12. };
  13. constructor() {
  14. this.sidebarId = SidebarManager.CONFIG.SIDEBAR_ID;
  15. this.isOpen = false;
  16. this.currentUrl = window.location.href;
  17. this.sidebarFrame = null;
  18. this.urlObserver = new UrlChangeObserver();
  19. this.setupUrlChangeHandlers();
  20. this.init();
  21. }
  22. /**
  23. * 初始化侧边栏
  24. */
  25. async init() {
  26. try {
  27. // 添加错误恢复机制
  28. const maxRetries = 3;
  29. let retries = 0;
  30. while (retries < maxRetries) {
  31. try {
  32. await this.checkAndRestoreState();
  33. this.setupEventListeners();
  34. this.observeTitleChanges();
  35. break; // 成功则跳出循环
  36. } catch (error) {
  37. retries++;
  38. if (retries === maxRetries) {
  39. throw error;
  40. }
  41. // 等待一段时间后重试
  42. await new Promise((resolve) => setTimeout(resolve, 1000));
  43. }
  44. }
  45. } catch (error) {
  46. console.warn("Sidebar initialization failed:", error);
  47. }
  48. }
  49. /**
  50. * 设置事件监听器
  51. */
  52. setupEventListeners() {
  53. // 监听页面加载完成事件
  54. window.addEventListener("load", () => this.checkAndRestoreState());
  55. // 监听来自 sidebar 的消息
  56. window.addEventListener("message", this.handleMessage.bind(this));
  57. // 监听窗口大小变化
  58. window.addEventListener(
  59. "resize",
  60. Utils.debounce(() => this.handleResize(), 100)
  61. );
  62. // 监听页面可见性变化
  63. document.addEventListener("visibilitychange", () => {
  64. if (!document.hidden) {
  65. this.checkAndRestoreState();
  66. }
  67. });
  68. // 监听 history 变化
  69. window.addEventListener("popstate", () => {
  70. this.checkAndRestoreState();
  71. });
  72. // 监听页面 URL 变化
  73. let lastUrl = location.href;
  74. new MutationObserver(() => {
  75. const url = location.href;
  76. if (url !== lastUrl) {
  77. lastUrl = url;
  78. this.checkAndRestoreState();
  79. }
  80. }).observe(document, { subtree: true, childList: true });
  81. // 监听来自sidebar的消息
  82. window.addEventListener("message", async (event) => {
  83. if (event.data.type === "COPY_TO_CLIPBOARD") {
  84. try {
  85. await navigator.clipboard.writeText(event.data.text);
  86. // 可选:发送成功消息回sidebar
  87. event.source.postMessage(
  88. {
  89. type: "COPY_SUCCESS",
  90. },
  91. "*"
  92. );
  93. } catch (err) {
  94. console.error("Failed to copy text:", err);
  95. // 可选:发送失败消息回sidebar
  96. event.source.postMessage(
  97. {
  98. type: "COPY_ERROR",
  99. error: err.message,
  100. },
  101. "*"
  102. );
  103. }
  104. }
  105. });
  106. // 监听来自iframe的消息
  107. window.addEventListener("message", (event) => {
  108. // 确保消息来自我们的iframe
  109. if (
  110. event.source ===
  111. document.getElementById("paiwise-sidebar")?.contentWindow
  112. ) {
  113. if (event.data.type === "ANALYZE_PAGE") {
  114. // 分析页面并返回结果
  115. const pageInfo = window.pageAnalyzer.analyzePage();
  116. // 发送分析结果回iframe
  117. event.source.postMessage(
  118. {
  119. type: "PAGE_ANALYSIS_RESULT",
  120. pageInfo: pageInfo,
  121. },
  122. "*"
  123. );
  124. }
  125. }
  126. });
  127. }
  128. /**
  129. * 处理接收到的消息
  130. * @param {MessageEvent} event
  131. */
  132. handleMessage(event) {
  133. if (event.data.action === "closeSidebar") {
  134. this.removeSidebar();
  135. }
  136. }
  137. /**
  138. * 处理窗口大小变化
  139. */
  140. handleResize() {
  141. const sidebar = document.getElementById(this.sidebarId);
  142. if (sidebar && this.isOpen) {
  143. sidebar.style.height = `${window.innerHeight}px`;
  144. this.adjustPageLayout();
  145. }
  146. }
  147. /**
  148. * 检查并恢复侧边栏状态
  149. */
  150. async checkAndRestoreState() {
  151. try {
  152. const isOpen = await Utils.getStorageData(
  153. SidebarManager.CONFIG.STORAGE_KEY
  154. );
  155. // 如果获取存储数据失败,默认为关闭状态
  156. if (isOpen === null) {
  157. return;
  158. }
  159. if (isOpen && !this.isOpen) {
  160. // 只有当应该打开且当前未打开时才创建
  161. this.createSidebar();
  162. } else if (!isOpen && this.isOpen) {
  163. // 只有当应该关闭且当前打开时才移除
  164. this.removeSidebar();
  165. }
  166. } catch (error) {
  167. console.warn("Failed to restore sidebar state:", error);
  168. // 出错时不做任何操作,保持当前状态
  169. }
  170. }
  171. /**
  172. * 发送页面信息到iframe
  173. */
  174. sendPageInfo() {
  175. const iframe = document.getElementById(this.sidebarId);
  176. if (!iframe) return;
  177. // 获取页面favicon
  178. let favicon = "";
  179. const iconLink = document.querySelector('link[rel*="icon"]');
  180. if (iconLink) {
  181. favicon = iconLink.href;
  182. } else {
  183. // 如果没有找到,使用网站根目录的favicon.ico
  184. const url = new URL(window.location.href);
  185. favicon = `${url.protocol}//${url.hostname}/favicon.ico`;
  186. }
  187. // 发送页面信息到iframe
  188. iframe.contentWindow.postMessage(
  189. {
  190. type: "PAGE_INFO",
  191. pageInfo: {
  192. favicon: favicon,
  193. title: document.title,
  194. url: window.location.href,
  195. },
  196. },
  197. "*"
  198. );
  199. }
  200. /**
  201. * 创建侧边栏
  202. */
  203. async createSidebar() {
  204. if (document.getElementById(this.sidebarId)) return;
  205. try {
  206. // 创建页面包装器
  207. let wrapper = document.querySelector(".page-wrapper");
  208. if (!wrapper) {
  209. wrapper = document.createElement("div");
  210. wrapper.className = "page-wrapper";
  211. // 保存body的当前内容
  212. const bodyContent = Array.from(document.body.children);
  213. // 将所有内容(除了已有的侧边栏)移动到包装器中
  214. bodyContent.forEach((child) => {
  215. if (child.id !== this.sidebarId) {
  216. wrapper.appendChild(child);
  217. }
  218. });
  219. // 添加包装器到body
  220. if (wrapper.hasChildNodes()) {
  221. document.body.insertBefore(wrapper, document.body.firstChild);
  222. }
  223. }
  224. this.sidebarFrame = document.createElement("iframe");
  225. this.sidebarFrame.id = this.sidebarId;
  226. this.sidebarFrame.src = chrome.runtime.getURL("sidebar.html");
  227. document.body.appendChild(this.sidebarFrame);
  228. document.body.classList.add("sidebar-open");
  229. // 确保在页面加载完成后重新应用布局
  230. if (document.readyState === "complete") {
  231. this.reapplyLayout();
  232. } else {
  233. window.addEventListener("load", () => this.reapplyLayout(), {
  234. once: true,
  235. });
  236. }
  237. // 等待iframe加载完成
  238. await new Promise((resolve) => {
  239. this.sidebarFrame.onload = () => {
  240. this.sendPageInfo();
  241. resolve();
  242. };
  243. });
  244. this.sidebarFrame.classList.add("show");
  245. this.isOpen = true;
  246. await Utils.setStorageData(SidebarManager.CONFIG.STORAGE_KEY, true);
  247. } catch (error) {
  248. console.error("Failed to create sidebar:", error);
  249. if (this.sidebarFrame) {
  250. this.sidebarFrame.remove();
  251. this.sidebarFrame = null;
  252. }
  253. document.body.classList.remove("sidebar-open");
  254. }
  255. }
  256. /**
  257. * 移除侧边栏
  258. */
  259. async removeSidebar() {
  260. if (!this.isOpen) return;
  261. try {
  262. if (this.sidebarFrame) {
  263. this.sidebarFrame.classList.remove("show");
  264. // 等待动画完成后再移除元素
  265. await new Promise((resolve) => {
  266. this.sidebarFrame.addEventListener(
  267. "transitionend",
  268. () => {
  269. document.documentElement.classList.remove("sidebar-open");
  270. this.sidebarFrame.remove();
  271. this.sidebarFrame = null;
  272. resolve();
  273. },
  274. { once: true }
  275. );
  276. });
  277. }
  278. this.isOpen = false;
  279. await Utils.setStorageData(SidebarManager.CONFIG.STORAGE_KEY, false);
  280. } catch (error) {
  281. console.error("Failed to remove sidebar:", error);
  282. }
  283. }
  284. /**
  285. * 切换侧边栏显示状态
  286. */
  287. toggle() {
  288. if (this.isOpen) {
  289. this.removeSidebar();
  290. } else {
  291. this.createSidebar();
  292. }
  293. }
  294. setupUrlChangeHandlers() {
  295. // 注册URL变化的处理函数
  296. this.urlObserver.addObserver(() => {
  297. // 更新页面信息
  298. this.updatePageInfo();
  299. // 重新应用布局
  300. this.reapplyLayout();
  301. });
  302. // 开始监听URL变化
  303. this.urlObserver.startObserving();
  304. }
  305. observeTitleChanges() {
  306. // 监听document.title的变化
  307. const titleObserver = new MutationObserver(() => {
  308. this.updatePageInfo();
  309. });
  310. titleObserver.observe(document.querySelector("head > title"), {
  311. subtree: true,
  312. characterData: true,
  313. childList: true,
  314. });
  315. }
  316. updatePageInfo() {
  317. const frame = this.sidebarFrame || document.getElementById(this.sidebarId);
  318. if (frame) {
  319. const pageInfo = {
  320. title: document.title,
  321. url: window.location.href,
  322. favicon: this.getFavicon(),
  323. };
  324. frame.contentWindow.postMessage(
  325. {
  326. type: "PAGE_INFO",
  327. pageInfo,
  328. },
  329. "*"
  330. );
  331. }
  332. }
  333. getFavicon() {
  334. // 尝试获取页面favicon
  335. const iconLink = document.querySelector("link[rel*='icon']");
  336. if (iconLink) return iconLink.href;
  337. // 如果没有找到,返回网站根目录的favicon.ico
  338. const url = new URL(window.location.href);
  339. return `${url.protocol}//${url.hostname}/favicon.ico`;
  340. }
  341. /**
  342. * 重新应用布局
  343. */
  344. reapplyLayout() {
  345. if (!this.isOpen) return;
  346. document.body.classList.remove("sidebar-open");
  347. requestAnimationFrame(() => {
  348. document.body.classList.add("sidebar-open");
  349. });
  350. }
  351. }
  352. class UrlChangeObserver {
  353. constructor() {
  354. this.observers = [];
  355. this.currentUrl = window.location.href;
  356. }
  357. // 添加观察者
  358. addObserver(observer) {
  359. this.observers.push(observer);
  360. }
  361. // 通知所有观察者
  362. notifyObservers(newUrl) {
  363. this.observers.forEach((observer) => observer(newUrl));
  364. }
  365. // 开始监听URL变化
  366. startObserving() {
  367. // 使用MutationObserver监听DOM变化
  368. const observer = new MutationObserver(() => {
  369. if (this.currentUrl !== window.location.href) {
  370. const newUrl = window.location.href;
  371. this.currentUrl = newUrl;
  372. this.notifyObservers(newUrl);
  373. }
  374. });
  375. // 监听整个文档的变化
  376. observer.observe(document.documentElement, {
  377. childList: true,
  378. subtree: true,
  379. });
  380. // 监听history变化
  381. window.addEventListener("popstate", () => {
  382. this.notifyObservers(window.location.href);
  383. });
  384. // 监听hashchange事件
  385. window.addEventListener("hashchange", () => {
  386. this.notifyObservers(window.location.href);
  387. });
  388. }
  389. }
  390. // 创建一个全局的URL观察者实例
  391. window.urlObserver = new UrlChangeObserver();
  392. // 初始化侧边栏管理器
  393. const sidebarManager = new SidebarManager();
  394. // 监听来自背景脚本的消息
  395. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  396. if (message.action === "toggleSidebar") {
  397. sidebarManager.toggle();
  398. sendResponse({ success: true });
  399. }
  400. });
  401. // 其他需要监听URL变化的功能也可以注册到观察者
  402. window.urlObserver.addObserver((newUrl) => {
  403. // 例如:更新总结卡片
  404. console.log("URL changed to:", newUrl);
  405. // 在这里添加更新总结卡片的逻辑
  406. });
  407. // 开始监听URL变化
  408. window.urlObserver.startObserving();
  409. // 创建一个新的js文件用于处理侧边栏内部的关闭按钮事件