From 6fc1a93ac2f3b82980270cda83853c8c206e17ef Mon Sep 17 00:00:00 2001 From: qcrao Date: Wed, 15 May 2024 12:03:10 +0800 Subject: [PATCH 01/15] support all languages && improved code readability and maintainability --- addCheckboxes.js | 115 ++++++++++++++++++------------------- bulkDeleteConversations.js | 38 +++++------- globals.js | 11 ++-- manifest.json | 2 +- removeCheckboxes.js | 18 +++--- toggleCheckboxes.js | 4 +- 6 files changed, 87 insertions(+), 101 deletions(-) diff --git a/addCheckboxes.js b/addCheckboxes.js index b44459e..aa21de1 100644 --- a/addCheckboxes.js +++ b/addCheckboxes.js @@ -1,16 +1,67 @@ console.log('addCheckboxes.js loaded'); -// 查找带有特定选择器的祖先元素 +// Add checkboxes to each conversation +function addCheckboxes() { + console.log('Adding checkboxes to conversations...', Selectors); + const conversations = document.querySelectorAll(Selectors.CONVERSATION_SELECTOR); + + conversations.forEach((conversation, index) => { + let checkbox = conversation.querySelector(`.${CHECKBOX_CLASS}`); + + // If the checkbox does not exist, create and insert it + if (!checkbox) { + checkbox = createCheckbox(index); + conversation.insertAdjacentElement('afterbegin', checkbox); + } + + // Add click event to the conversation title + const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR); + if (titleElement) { + titleElement.style.cursor = 'default'; + + // Add click event listener if not already added + if (!titleElement.dataset.hasClickListener) { + addClickEventListener(titleElement); + } + + // Add click event listener to the parent element if not already added + const parentElement = titleElement.parentElement; + if (parentElement && !parentElement.dataset.hasClickListener) { + parentElement.style.cursor = 'default'; + addClickEventListener(parentElement); + } + } + }); +} + +// Create a new checkbox element +function createCheckbox(index) { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = CHECKBOX_CLASS; + checkbox.dataset.index = index; + checkbox.addEventListener('click', preventEventPropagation); + return checkbox; +} + +// Add click event listener to an element +function addClickEventListener(element) { + const handleTitleClick = (event) => { + toggleCheckbox(event); + event.stopPropagation(); + }; + element.addEventListener('click', handleTitleClick); + element.dataset.hasClickListener = 'true'; +} + function findAncestorWithCheckbox(el, selector) { while ((el = el.parentElement) && !el.querySelector(selector)); return el; } -// 切换复选框的选中状态 +// Toggle the checkbox's checked state function toggleCheckbox(event) { - // 阻止事件的默认行为(例如链接跳转) event.preventDefault(); - // 阻止事件冒泡到父元素 event.stopPropagation(); const parentElement = findAncestorWithCheckbox(event.currentTarget, `.${CHECKBOX_CLASS}`); @@ -18,66 +69,10 @@ function toggleCheckbox(event) { if (checkbox) { checkbox.checked = !checkbox.checked; } - event.stopPropagation(); } -// 阻止事件冒泡 function preventEventPropagation(event) { event.stopPropagation(); } -// 添加复选框到每个对话 -function addCheckboxes() { - console.log('Adding checkboxes to conversations...', Selectors); - const conversations = document.querySelectorAll(Selectors.CONVERSATION_SELECTOR); - - conversations.forEach((conversation, index) => { - let existingCheckbox = conversation.querySelector(`.${CHECKBOX_CLASS}`); - - // 如果复选框已存在,获取其选中状态并移除它 - let isChecked = existingCheckbox ? existingCheckbox.checked : false; - if (existingCheckbox) { - existingCheckbox.remove(); - } - - // 创建新的复选框并设置其属性 - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = CHECKBOX_CLASS; - checkbox.dataset.index = index; - checkbox.checked = isChecked; - checkbox.addEventListener('click', preventEventPropagation); - conversation.insertAdjacentElement('afterbegin', checkbox); - - // 为对话标题添加点击事件 - const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR); - if (titleElement) { - titleElement.style.cursor = 'default'; - - // 获取 titleElement 的父元素 - const parentElement = titleElement.parentElement; - - // 定义一个通用的事件处理函数 - const handleTitleClick = (event) => { - toggleCheckbox(event); - event.stopPropagation(); // 防止事件冒泡 - }; - - // 为 titleElement 添加点击事件 - if (!titleElement.dataset.hasClickListener) { - titleElement.addEventListener('click', handleTitleClick); - titleElement.dataset.hasClickListener = 'true'; - } - - // 为 titleElement 的父元素添加点击事件 - if (parentElement && !parentElement.dataset.hasClickListener) { - parentElement.style.cursor = 'default'; - parentElement.addEventListener('click', handleTitleClick); - parentElement.dataset.hasClickListener = 'true'; - } - } - }); -} - -// 执行主函数 addCheckboxes(); diff --git a/bulkDeleteConversations.js b/bulkDeleteConversations.js index 92b9b96..9ef07a0 100644 --- a/bulkDeleteConversations.js +++ b/bulkDeleteConversations.js @@ -21,18 +21,14 @@ function getSelectedConversations() { } function removeAllCheckboxes() { - const allCheckboxes = document.querySelectorAll(Selectors.conversationsCheckbox); + const allCheckboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`); allCheckboxes.forEach(checkbox => checkbox.remove()); } async function deleteConversation(checkbox) { - const conversationElement = checkbox.parentElement; await delay(100); - // console.log("1. Clicking conversation...", conversationElement); - // conversationElement.click(); - // 将 click 替换为悬停 - // 创建一个鼠标悬停事件 + const conversationElement = checkbox.parentElement; const hoverEvent = new MouseEvent('mouseover', { view: window, bubbles: true, @@ -40,7 +36,6 @@ async function deleteConversation(checkbox) { }); console.log("1. Hovering over conversation...", conversationElement); - // 触发鼠标悬停事件 conversationElement.dispatchEvent(hoverEvent); await delay(200); @@ -50,9 +45,9 @@ async function deleteConversation(checkbox) { pointerType: 'mouse' }); const threeDotButton = await waitForElement(Selectors.threeDotButton, conversationElement.parentElement); + console.log("2. Clicking three dot button...", threeDotButton); threeDotButton.dispatchEvent(pointerDownEvent); await delay(300); - console.log("2. Clicking three dot button...", threeDotButton); const deleteButton = await waitForDeleteButton(); @@ -60,8 +55,8 @@ async function deleteConversation(checkbox) { console.log("3. Clicking delete button...", deleteButton); deleteButton.click(); - const confirmButton = await waitForElement(Selectors.confirmDeleteButton); + const confirmButton = await waitForElement(Selectors.confirmDeleteButton); if (confirmButton) { console.log("4. Clicking confirm button..."); confirmButton.click(); @@ -74,22 +69,22 @@ async function deleteConversation(checkbox) { } async function waitForDeleteButton(parent = document, timeout = 2000) { - const selector = 'div[role="menuitem"]'; // 设定好选择器 - const textContent = "Delete"; // 指定文本内容 + const selector = 'div[role="menuitem"]'; + const textContent = "Delete"; const startedAt = Date.now(); while ((Date.now() - startedAt) < timeout) { const elements = parent.querySelectorAll(selector); - const element = Array.from(elements).find(el => el.textContent.trim() === textContent); - if (element) return element; // 返回找到的元素 + const element = Array.from(elements).find(el => + el.textContent.trim() === textContent || el.querySelector('.text-token-text-error') + ); + if (element) return element; await delay(100); } - - console.log(`Delete button not found within ${timeout}ms`); - throw new Error(`Delete button not found within ${timeout}ms`); + + return null; } -// Helper function to create a delay function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -98,10 +93,10 @@ async function waitForElement(selector, parent = document, timeout = 2000) { const startedAt = Date.now(); while ((Date.now() - startedAt) < timeout) { const element = parent.querySelector(selector); - if (element) return element; // 返回找到的元素 + if (element) return element; await delay(100); } - console.log(`Element ${selector} not found within ${timeout}ms in the specified parent`); + throw new Error(`Element ${selector} not found within ${timeout}ms in the specified parent`); } @@ -112,11 +107,8 @@ async function waitForElementToDisappear(selector, timeout = 2000) { if (!element) return; await delay(100); } + throw new Error(`Element ${selector} did not disappear within ${timeout}ms`); } -function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - bulkDeleteConversations(); diff --git a/globals.js b/globals.js index e8ad938..7f89c6a 100644 --- a/globals.js +++ b/globals.js @@ -3,23 +3,22 @@ if (typeof window.globalsLoaded === 'undefined') { window.globalsLoaded = true; - let lastChecked = null; + const lastChecked = null; - let Selectors = { - // Plus 用户的选择器 + const Selectors = { conversationsCheckbox: '.conversation-checkbox:checked', confirmDeleteButton: 'button.btn.btn-danger', threeDotButton: '[id^="radix-"]', - // 其他 Plus 用户选择器... CONVERSATION_SELECTOR: 'div > div > div > div > div > div > nav > div > div > div > div > ol > li > div > a', TITLE_SELECTOR: '.relative.grow.overflow-hidden.whitespace-nowrap', }; - let CHECKBOX_CLASS = 'conversation-checkbox'; + const CHECKBOX_CLASS = 'conversation-checkbox'; + // Expose variables to the global scope window.lastChecked = lastChecked; window.Selectors = Selectors; window.CHECKBOX_CLASS = CHECKBOX_CLASS; } else { console.log('globals.js already loaded, skipping re-initialization'); -} \ No newline at end of file +} diff --git a/manifest.json b/manifest.json index 313bed3..eedca8a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ChatGPT Bulk Delete", - "version": "4.5", + "version": "4.6", "description": "A Chrome extension to bulk delete ChatGPT conversations", "icons": { "48": "icon48.png" diff --git a/removeCheckboxes.js b/removeCheckboxes.js index 5289ef6..66fb57f 100644 --- a/removeCheckboxes.js +++ b/removeCheckboxes.js @@ -1,9 +1,9 @@ -(function() { - const removeConversationCheckboxes = document.querySelectorAll('.conversation-checkbox'); - removeConversationCheckboxes.forEach(checkbox => { - checkbox.remove(); - }); - // 在移除所有复选框后刷新页面 - location.reload(); - })(); - \ No newline at end of file +function removeCheckboxesAndReload() { + const checkboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`); + checkboxes.forEach(checkbox => checkbox.remove()); + + // Refresh the page after removing all checkboxes + location.reload(); +} + +removeCheckboxesAndReload(); \ No newline at end of file diff --git a/toggleCheckboxes.js b/toggleCheckboxes.js index 18c464d..78b30d1 100644 --- a/toggleCheckboxes.js +++ b/toggleCheckboxes.js @@ -1,9 +1,9 @@ function toggleCheckboxes() { - const conversations = document.querySelectorAll(".conversation-checkbox"); + const conversations = document.querySelectorAll(`.${CHECKBOX_CLASS}`); conversations.forEach((checkbox) => { checkbox.checked = !checkbox.checked; }); } -toggleCheckboxes(); +toggleCheckboxes(); \ No newline at end of file From bf3795ec7ee20b5cdd5ef2d4c4efcf2fde848d86 Mon Sep 17 00:00:00 2001 From: qcrao Date: Mon, 8 Jul 2024 12:58:34 +0800 Subject: [PATCH 02/15] add bulk archive button --- bulkArchiveCoversations.js | 100 +++++++++++++++++++++++++++++++++++++ popup.css | 39 +++++++++------ popup.html | 72 ++++++++++++++++---------- 3 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 bulkArchiveCoversations.js diff --git a/bulkArchiveCoversations.js b/bulkArchiveCoversations.js new file mode 100644 index 0000000..be4e0f8 --- /dev/null +++ b/bulkArchiveCoversations.js @@ -0,0 +1,100 @@ +console.log("bulkArchiveConversations.js loaded"); + +async function bulkArchiveConversations() { + const selectedConversations = getSelectedConversations(); + + if (selectedConversations.length === 0) { + console.log("No conversations to archive."); + removeAllCheckboxes(); + return; + } + + console.log("Selected Conversations:", selectedConversations); + + for (const element of selectedConversations) { + await archiveConversation(element); + } +} + +function getSelectedConversations() { + return [...document.querySelectorAll(Selectors.conversationsCheckbox)]; +} + +function removeAllCheckboxes() { + const allCheckboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`); + allCheckboxes.forEach((checkbox) => checkbox.remove()); +} + +async function archiveConversation(checkbox) { + await delay(100); + + const conversationElement = checkbox.parentElement; + const hoverEvent = new MouseEvent("mouseover", { + view: window, + bubbles: true, + cancelable: true, + }); + + console.log("1. Hovering over conversation...", conversationElement); + conversationElement.dispatchEvent(hoverEvent); + await delay(200); + + const pointerDownEvent = new PointerEvent("pointerdown", { + bubbles: true, + cancelable: true, + pointerType: "mouse", + }); + const threeDotButton = await waitForElement( + Selectors.threeDotButton, + conversationElement.parentElement + ); + console.log("2. Clicking three dot button...", threeDotButton); + threeDotButton.dispatchEvent(pointerDownEvent); + await delay(300); + + const archiveButton = await waitForArchiveButton(); + + if (archiveButton) { + console.log("3. Clicking archive button...", archiveButton); + archiveButton.click(); + await delay(500); + } + + console.log("4. Archiving completed."); +} + +async function waitForArchiveButton(parent = document, timeout = 2000) { + const selector = 'div[role="menuitem"]'; + const textContent = "Archive"; + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeout) { + const elements = parent.querySelectorAll(selector); + const element = Array.from(elements).find( + (el) => el.textContent.trim() === textContent + ); + if (element) return element; + await delay(100); + } + + return null; +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForElement(selector, parent = document, timeout = 2000) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeout) { + const element = parent.querySelector(selector); + if (element) return element; + await delay(100); + } + + throw new Error( + `Element ${selector} not found within ${timeout}ms in the specified parent` + ); +} + +bulkArchiveConversations(); diff --git a/popup.css b/popup.css index b493d13..030698a 100644 --- a/popup.css +++ b/popup.css @@ -1,10 +1,10 @@ body { - width: 240px; - padding: 15px; - font-family: Arial, sans-serif; - display: flex; - flex-direction: column; - align-items: center; + width: 240px; + padding: 15px; + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; } .header { @@ -33,9 +33,9 @@ button { padding: 8px 12px; font-size: 14px; cursor: pointer; - background-color: #1ABC9C; + background-color: #1abc9c; color: white; - border: 1px solid #16A085; + border: 1px solid #16a085; border-radius: 6px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); @@ -44,17 +44,21 @@ button { } button:hover { - background-color: #16A085; - border-color: #14967F; + background-color: #16a085; + border-color: #14967f; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); } -button#bulk-delete, button#toggle-checkboxes { +button#bulk-delete, +button#toggle-checkboxes, +button#bulk-archive { font-size: 16px; background-color: #e74c3c; border-color: #c0392b; margin-top: 10px; - width: calc(200px + 40px); /* bulk-delete 宽度等于 body 的宽度减去左右内边距再加上两者之间的距离 */ + width: calc( + 200px + 40px + ); /* bulk-delete 宽度等于 body 的宽度减去左右内边距再加上两者之间的距离 */ } .buttons-wrapper { @@ -94,12 +98,17 @@ button#toggle-checkboxes { border-color: #2758ab; } +button#bulk-archive { + background-color: #da6922; + border-color: #dc661c; +} + #sponsorLink { display: inline-flex; align-items: center; padding: 10px 20px; - background-color: #383E46; - border-color: #78838F; + background-color: #383e46; + border-color: #78838f; border-radius: 5px; color: #fff; text-decoration: none; @@ -114,4 +123,4 @@ button#toggle-checkboxes { #sponsorLink:hover { background-color: #993366; -} \ No newline at end of file +} diff --git a/popup.html b/popup.html index 8d2cc6d..7ee88e6 100644 --- a/popup.html +++ b/popup.html @@ -1,34 +1,56 @@ - - - + + + ChatGPT Bulk Delete - - - + + +
-

bulk-delete-chatGPT

+

bulk-delete-chatGPT

-
- - -
- - -
- - - + + + - + From b733dbe9c0e2dac1f6f5b1798788a3ad900bdf6a Mon Sep 17 00:00:00 2001 From: qcrao Date: Mon, 8 Jul 2024 15:52:37 +0800 Subject: [PATCH 03/15] beautify css by claude --- popup.css | 192 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 76 deletions(-) diff --git a/popup.css b/popup.css index 030698a..fffb377 100644 --- a/popup.css +++ b/popup.css @@ -1,79 +1,125 @@ body { - width: 240px; - padding: 15px; - font-family: Arial, sans-serif; + width: 280px; + padding: 20px; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; + background-color: #f0f2f5; + color: #333; } .header { - margin-bottom: 10px; + margin-bottom: 20px; width: 100%; - border-bottom: 1px solid #ccc; - padding-bottom: 10px; + border-bottom: 2px solid #e1e4e8; + padding-bottom: 15px; } .header h1 { - font-size: 16px; - font-weight: bold; + font-size: 18px; + font-weight: 600; text-align: center; margin: 0; -} - -.buttons-row { - display: flex; - justify-content: space-between; - width: 100%; - margin-bottom: 10px; - align-items: flex-start; -} - -button { - padding: 8px 12px; - font-size: 14px; - cursor: pointer; - background-color: #1abc9c; - color: white; - border: 1px solid #16a085; - border-radius: 6px; - text-align: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition-duration: 0.4s; - margin: 0; /* 去掉左右外边距 */ -} - -button:hover { - background-color: #16a085; - border-color: #14967f; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); -} - -button#bulk-delete, -button#toggle-checkboxes, -button#bulk-archive { - font-size: 16px; - background-color: #e74c3c; - border-color: #c0392b; - margin-top: 10px; - width: calc( - 200px + 40px - ); /* bulk-delete 宽度等于 body 的宽度减去左右内边距再加上两者之间的距离 */ + color: #2c3e50; } .buttons-wrapper { display: flex; flex-direction: column; width: 100%; - margin-top: 10px; + gap: 15px; +} + +.buttons-row { + display: flex; + justify-content: space-between; + width: 100%; + gap: 10px; +} + +button { + padding: 10px 15px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + outline: none; + position: relative; + overflow: hidden; +} + +button::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 5px; + height: 5px; + background: rgba(255, 255, 255, 0.5); + opacity: 0; + border-radius: 100%; + transform: scale(1, 1) translate(-50%); + transform-origin: 50% 50%; +} + +button:hover { + transform: translateY(-3px); + box-shadow: 0 7px 14px rgba(0, 0, 0, 0.18); +} + +button:hover::after { + animation: ripple 1s ease-out; +} + +@keyframes ripple { + 0% { + transform: scale(0, 0); + opacity: 0.5; + } + 20% { + transform: scale(25, 25); + opacity: 0.3; + } + 100% { + opacity: 0; + transform: scale(40, 40); + } } .buttons-row button { - width: 120px; /* 将 remove-checkboxes 和 add-checkboxes 按钮的宽度增加到 120px */ + flex: 1; } -.buttons-row button:first-child { - margin-right: 10px; /* 添加一个右外边距来增加两个按钮之间的距离 */ +button#bulk-delete, +button#toggle-checkboxes, +button#bulk-archive { + font-size: 16px; + width: 100%; + margin-top: 5px; + padding: 12px 15px; +} + +button#add-checkboxes { + background-color: #2ecc71; +} +button#remove-checkboxes { + background-color: #e74c3c; +} +button#toggle-checkboxes { + background-color: #f39c12; +} +button#bulk-delete { + background-color: #e74c3c; +} +button#bulk-archive { + background-color: #9b59b6; } .footer { @@ -81,46 +127,40 @@ button#bulk-archive { width: 100%; font-size: 12px; text-align: center; - border-top: 1px solid #ccc; - padding-top: 10px; - display: flex; /* 添加这一行使得.footer下的直接子元素并排排列 */ - justify-content: center; /* 使得子元素在中心对齐 */ - gap: 10px; /* 如果需要,可以添加这一行以在子元素之间创建一些空间 */ -} - -button#remove-checkboxes { - background-color: #badb34; - border-color: #b6b929; -} - -button#toggle-checkboxes { - background-color: #2287da; - border-color: #2758ab; -} - -button#bulk-archive { - background-color: #da6922; - border-color: #dc661c; + border-top: 2px solid #e1e4e8; + padding-top: 15px; + display: flex; + justify-content: center; + gap: 20px; } #sponsorLink { display: inline-flex; align-items: center; - padding: 10px 20px; - background-color: #383e46; - border-color: #78838f; + padding: 8px 15px; + background-color: #34495e; border-radius: 5px; color: #fff; text-decoration: none; cursor: pointer; - transition: background-color 0.3s ease; + transition: all 0.3s ease; + font-weight: 500; +} + +#sponsorLink:hover { + background-color: #2c3e50; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } #sponsorLink .icon-text { display: flex; align-items: center; + gap: 8px; } #sponsorLink:hover { background-color: #993366; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } From 2a2844eeccedcc90246e70ec4d222ee3e0e1954e Mon Sep 17 00:00:00 2001 From: qcrao Date: Mon, 8 Jul 2024 17:05:50 +0800 Subject: [PATCH 04/15] add bulk archive --- ...rsations.js => bulkArchiveConversations.js | 8 +++- bulkDeleteConversations.js | 37 +++++++++++-------- popup.js | 35 ++++++++++-------- 3 files changed, 49 insertions(+), 31 deletions(-) rename bulkArchiveCoversations.js => bulkArchiveConversations.js (90%) diff --git a/bulkArchiveCoversations.js b/bulkArchiveConversations.js similarity index 90% rename from bulkArchiveCoversations.js rename to bulkArchiveConversations.js index be4e0f8..292e58f 100644 --- a/bulkArchiveCoversations.js +++ b/bulkArchiveConversations.js @@ -66,12 +66,18 @@ async function archiveConversation(checkbox) { async function waitForArchiveButton(parent = document, timeout = 2000) { const selector = 'div[role="menuitem"]'; const textContent = "Archive"; + const textContentSimplifiedChinese = "归档"; + const textContentTraditionalChinese = "封存"; + const startedAt = Date.now(); while (Date.now() - startedAt < timeout) { const elements = parent.querySelectorAll(selector); const element = Array.from(elements).find( - (el) => el.textContent.trim() === textContent + (el) => + el.textContent.trim() === textContent || + el.textContent.trim() === textContentSimplifiedChinese || + el.textContent.trim() === textContentTraditionalChinese ); if (element) return element; await delay(100); diff --git a/bulkDeleteConversations.js b/bulkDeleteConversations.js index 9ef07a0..205cde1 100644 --- a/bulkDeleteConversations.js +++ b/bulkDeleteConversations.js @@ -1,4 +1,4 @@ -console.log('bulkDeleteConversations.js loaded'); +console.log("bulkDeleteConversations.js loaded"); async function bulkDeleteConversations() { const selectedConversations = getSelectedConversations(); @@ -22,29 +22,32 @@ function getSelectedConversations() { function removeAllCheckboxes() { const allCheckboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`); - allCheckboxes.forEach(checkbox => checkbox.remove()); + allCheckboxes.forEach((checkbox) => checkbox.remove()); } async function deleteConversation(checkbox) { await delay(100); const conversationElement = checkbox.parentElement; - const hoverEvent = new MouseEvent('mouseover', { + const hoverEvent = new MouseEvent("mouseover", { view: window, bubbles: true, - cancelable: true + cancelable: true, }); console.log("1. Hovering over conversation...", conversationElement); - conversationElement.dispatchEvent(hoverEvent); + conversationElement.dispatchEvent(hoverEvent); await delay(200); - const pointerDownEvent = new PointerEvent('pointerdown', { + const pointerDownEvent = new PointerEvent("pointerdown", { bubbles: true, cancelable: true, - pointerType: 'mouse' + pointerType: "mouse", }); - const threeDotButton = await waitForElement(Selectors.threeDotButton, conversationElement.parentElement); + const threeDotButton = await waitForElement( + Selectors.threeDotButton, + conversationElement.parentElement + ); console.log("2. Clicking three dot button...", threeDotButton); threeDotButton.dispatchEvent(pointerDownEvent); await delay(300); @@ -73,10 +76,12 @@ async function waitForDeleteButton(parent = document, timeout = 2000) { const textContent = "Delete"; const startedAt = Date.now(); - while ((Date.now() - startedAt) < timeout) { + while (Date.now() - startedAt < timeout) { const elements = parent.querySelectorAll(selector); - const element = Array.from(elements).find(el => - el.textContent.trim() === textContent || el.querySelector('.text-token-text-error') + const element = Array.from(elements).find( + (el) => + el.textContent.trim() === textContent || + el.querySelector(".text-token-text-error") ); if (element) return element; await delay(100); @@ -86,23 +91,25 @@ async function waitForDeleteButton(parent = document, timeout = 2000) { } function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } async function waitForElement(selector, parent = document, timeout = 2000) { const startedAt = Date.now(); - while ((Date.now() - startedAt) < timeout) { + while (Date.now() - startedAt < timeout) { const element = parent.querySelector(selector); if (element) return element; await delay(100); } - throw new Error(`Element ${selector} not found within ${timeout}ms in the specified parent`); + throw new Error( + `Element ${selector} not found within ${timeout}ms in the specified parent` + ); } async function waitForElementToDisappear(selector, timeout = 2000) { const startedAt = Date.now(); - while ((Date.now() - startedAt) < timeout) { + while (Date.now() - startedAt < timeout) { const element = document.querySelector(selector); if (!element) return; await delay(100); diff --git a/popup.js b/popup.js index 6d31fc6..517094a 100644 --- a/popup.js +++ b/popup.js @@ -1,17 +1,20 @@ function loadGlobalsThenExecute(tabId, secondaryScript) { - chrome.scripting.executeScript({ - target: { tabId: tabId }, - files: ['globals.js'] - }, () => { - chrome.scripting.executeScript({ + chrome.scripting.executeScript( + { target: { tabId: tabId }, - files: [secondaryScript] - }); - }); + files: ["globals.js"], + }, + () => { + chrome.scripting.executeScript({ + target: { tabId: tabId }, + files: [secondaryScript], + }); + } + ); } function addButtonListener(buttonId, scriptName) { - document.getElementById(buttonId).addEventListener('click', () => { + document.getElementById(buttonId).addEventListener("click", () => { chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { if (tab) { loadGlobalsThenExecute(tab.id, scriptName); @@ -21,16 +24,18 @@ function addButtonListener(buttonId, scriptName) { } function initializeButtons() { - addButtonListener('add-checkboxes', 'addCheckboxes.js'); - addButtonListener('bulk-delete', 'bulkDeleteConversations.js'); - addButtonListener('toggle-checkboxes', 'toggleCheckboxes.js'); - addButtonListener('remove-checkboxes', 'removeCheckboxes.js'); + addButtonListener("add-checkboxes", "addCheckboxes.js"); + addButtonListener("bulk-delete", "bulkDeleteConversations.js"); + addButtonListener("toggle-checkboxes", "toggleCheckboxes.js"); + addButtonListener("remove-checkboxes", "removeCheckboxes.js"); + addButtonListener("bulk-archive", "bulkArchiveConversations.js"); } function updateCopyrightYear() { const currentYear = new Date().getFullYear(); - document.getElementById('copyright').innerHTML = - `© ${currentYear} qcrao@GitHub`; + document.getElementById( + "copyright" + ).innerHTML = `© ${currentYear} qcrao@GitHub`; } initializeButtons(); From ab244e8f4a3f6d218d089e4d8508bc6342f503dc Mon Sep 17 00:00:00 2001 From: qcrao Date: Mon, 8 Jul 2024 20:37:27 +0800 Subject: [PATCH 05/15] beautify lock logo position --- popup.css | 8 ++++++++ popup.html | 9 +++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/popup.css b/popup.css index fffb377..36b37eb 100644 --- a/popup.css +++ b/popup.css @@ -104,6 +104,14 @@ button#bulk-archive { width: 100%; margin-top: 5px; padding: 12px 15px; + position: relative; +} + +button#bulk-archive span { + position: absolute; + left: 75px; + top: 50%; + transform: translateY(-50%); } button#add-checkboxes { diff --git a/popup.html b/popup.html index 7ee88e6..d3e0223 100644 --- a/popup.html +++ b/popup.html @@ -30,8 +30,7 @@ + id="sponsorLink">  Sponsor From 0857a73d1addd28d20cb4d7679f4198e41b18d92 Mon Sep 17 00:00:00 2001 From: qcrao Date: Tue, 9 Jul 2024 08:34:46 +0800 Subject: [PATCH 06/15] add permissions --- manifest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manifest.json b/manifest.json index eedca8a..2b18411 100644 --- a/manifest.json +++ b/manifest.json @@ -13,17 +13,17 @@ }, "permissions": [ "scripting", - "activeTab" + "activeTab", + "https://bulk-delete-chatgpt-worker.qcrao.com/*" ], "content_scripts": [ { - "matches": [ - "*://chat.openai.com/*" - ], + "matches": ["*://chat.openai.com/*"], "js": [ "globals.js", "addCheckboxes.js", - "bulkDeleteConversations.js" + "bulkDeleteConversations.js", + "deleteConversations.js" ], "run_at": "document_idle" } From dae72c6e66570da9145462067d3af7a16bb27994 Mon Sep 17 00:00:00 2001 From: qcrao Date: Tue, 9 Jul 2024 15:13:49 +0800 Subject: [PATCH 07/15] add bulk archive --- manifest.json | 11 ++--- popup.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/manifest.json b/manifest.json index 2b18411..c0cec55 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ChatGPT Bulk Delete", - "version": "4.6", + "version": "5.0", "description": "A Chrome extension to bulk delete ChatGPT conversations", "icons": { "48": "icon48.png" @@ -11,11 +11,8 @@ "default_popup": "popup.html", "default_title": "Bulk Delete Conversations" }, - "permissions": [ - "scripting", - "activeTab", - "https://bulk-delete-chatgpt-worker.qcrao.com/*" - ], + "permissions": ["scripting", "activeTab", "identity", "identity.email"], + "host_permissions": ["https://bulk-delete-chatgpt-worker.qcrao.com/*"], "content_scripts": [ { "matches": ["*://chat.openai.com/*"], @@ -23,7 +20,7 @@ "globals.js", "addCheckboxes.js", "bulkDeleteConversations.js", - "deleteConversations.js" + "bulkArchiveConversations.js" ], "run_at": "document_idle" } diff --git a/popup.js b/popup.js index 517094a..3049028 100644 --- a/popup.js +++ b/popup.js @@ -28,7 +28,100 @@ function initializeButtons() { addButtonListener("bulk-delete", "bulkDeleteConversations.js"); addButtonListener("toggle-checkboxes", "toggleCheckboxes.js"); addButtonListener("remove-checkboxes", "removeCheckboxes.js"); - addButtonListener("bulk-archive", "bulkArchiveConversations.js"); + + const bulkArchiveButton = document.getElementById("bulk-archive"); + bulkArchiveButton.addEventListener("click", handleBulkArchive); +} + +async function checkMembershipStatus() { + const userInfo = await getUserInfo(); + if (!userInfo) { + console.error("Unable to get user info"); + updateBulkArchiveButton(false); + return; + } + + try { + const response = await fetch( + `https://bulk-delete-chatgpt-worker.qcrao.com/check-payment-status?user_id=${encodeURIComponent( + userInfo.id + )}` + ); + const data = await response.json(); + updateBulkArchiveButton(data.isPaid); + } catch (error) { + console.error("Error checking membership status:", error); + updateBulkArchiveButton(false); + } +} + +function updateBulkArchiveButton(isPaid) { + const bulkArchiveButton = document.getElementById("bulk-archive"); + if (isPaid) { + bulkArchiveButton.classList.remove("locked"); + bulkArchiveButton.querySelector("span").textContent = ""; + } else { + bulkArchiveButton.classList.add("locked"); + bulkArchiveButton.querySelector("span").textContent = "🔒"; + } +} + +async function handleBulkArchive() { + const userInfo = await getUserInfo(); + if (!userInfo) { + console.error("Unable to get user info"); + alert("Unable to verify user. Please try again later."); + return; + } + + const response = await fetch( + `https://bulk-delete-chatgpt-worker.qcrao.com/check-payment-status?user_id=${encodeURIComponent( + userInfo.id + )}` + ); + const data = await response.json(); + + if (data.isPaid) { + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (tab) { + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ["bulkArchiveConversations.js"], + }); + } + }); + } else { + if (confirm("一次性付费 0.99 USD,购买权限。是否继续?")) { + const payResponse = await fetch( + `https://bulk-delete-chatgpt-worker.qcrao.com/pay-bulk-archive?user_id=${encodeURIComponent( + userInfo.id + )}`, + { + method: "POST", + } + ); + const payData = await payResponse.json(); + console.log("payData", payData); + if (payData.paymentUrl) { + window.open(payData.paymentUrl, "_blank"); + } else { + alert("获取支付链接失败,请稍后再试。"); + } + } + } +} + +function getUserInfo() { + return new Promise((resolve) => { + chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (userInfo) => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + resolve(null); + } else { + resolve(userInfo); + } + }); + }); } function updateCopyrightYear() { @@ -38,5 +131,17 @@ function updateCopyrightYear() { ).innerHTML = `© ${currentYear} qcrao@GitHub`; } -initializeButtons(); -updateCopyrightYear(); +document.addEventListener("DOMContentLoaded", function () { + initializeButtons(); + updateCopyrightYear(); + checkMembershipStatus(); +}); + +// 每次打开popup时检查会员状态 +chrome.runtime.onConnect.addListener(function (port) { + if (port.name === "popup") { + port.onDisconnect.addListener(function () { + checkMembershipStatus(); + }); + } +}); From bbd81750c9c20820be1244e95323705e1c1b4fcb Mon Sep 17 00:00:00 2001 From: qcrao Date: Tue, 9 Jul 2024 15:55:56 +0800 Subject: [PATCH 08/15] beautify bug dialog --- popup.css | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ popup.html | 24 ++++++++++++++--- popup.js | 57 +++++++++++++++++++++++++++++----------- 3 files changed, 140 insertions(+), 18 deletions(-) diff --git a/popup.css b/popup.css index 36b37eb..6400c94 100644 --- a/popup.css +++ b/popup.css @@ -172,3 +172,80 @@ button#bulk-archive { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: #fefefe; + margin: 5% auto; + padding: 0; + border-radius: 10px; + width: 260px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + text-align: center; + overflow: hidden; +} + +.modal h2 { + margin: 0; + padding: 15px 0; + background-color: #4a4a4a; + color: white; + font-size: 18px; + width: 100%; +} + +.modal p { + color: #666; + font-size: 14px; + margin: 20px 15px; +} + +.modal-separator { + height: 1px; + background-color: #e0e0e0; + margin: 15px 0; +} + +.modal-buttons { + display: flex; + justify-content: center; + gap: 10px; + padding-bottom: 20px; +} + +.modal-button { + padding: 8px 16px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.modal-button-primary { + background-color: #007bff; + color: white; +} + +.modal-button-primary:hover { + background-color: #0056b3; +} + +#modalCancel { + background-color: #f8f9fa; + color: #333; +} + +#modalCancel:hover { + background-color: #e2e6ea; +} diff --git a/popup.html b/popup.html index d3e0223..d6830da 100644 --- a/popup.html +++ b/popup.html @@ -30,7 +30,8 @@ + id="sponsorLink" + >  Sponsor + + diff --git a/popup.js b/popup.js index 3049028..57aee50 100644 --- a/popup.js +++ b/popup.js @@ -91,26 +91,53 @@ async function handleBulkArchive() { } }); } else { - if (confirm("一次性付费 0.99 USD,购买权限。是否继续?")) { - const payResponse = await fetch( - `https://bulk-delete-chatgpt-worker.qcrao.com/pay-bulk-archive?user_id=${encodeURIComponent( - userInfo.id - )}`, - { - method: "POST", + showModal().then(async (result) => { + if (result) { + const payResponse = await fetch( + `https://bulk-delete-chatgpt-worker.qcrao.com/pay-bulk-archive?user_id=${encodeURIComponent( + userInfo.id + )}`, + { method: "POST" } + ); + const payData = await payResponse.json(); + console.log("payData", payData); + if (payData.paymentUrl) { + window.open(payData.paymentUrl, "_blank"); + } else { + alert("Failed to get payment link. Please try again later."); } - ); - const payData = await payResponse.json(); - console.log("payData", payData); - if (payData.paymentUrl) { - window.open(payData.paymentUrl, "_blank"); - } else { - alert("获取支付链接失败,请稍后再试。"); } - } + }); } } +function showModal() { + return new Promise((resolve) => { + const modal = document.getElementById("customModal"); + const okButton = document.getElementById("modalOK"); + const cancelButton = document.getElementById("modalCancel"); + + modal.style.display = "block"; + + okButton.onclick = () => { + modal.style.display = "none"; + resolve(true); + }; + + cancelButton.onclick = () => { + modal.style.display = "none"; + resolve(false); + }; + + window.onclick = (event) => { + if (event.target == modal) { + modal.style.display = "none"; + resolve(false); + } + }; + }); +} + function getUserInfo() { return new Promise((resolve) => { chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (userInfo) => { From 3fc04ad03e1e2c3fee401b216437c40e5805384f Mon Sep 17 00:00:00 2001 From: qcrao Date: Tue, 9 Jul 2024 23:14:52 +0800 Subject: [PATCH 09/15] update title of extension --- popup.html | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/popup.html b/popup.html index d6830da..b7ece00 100644 --- a/popup.html +++ b/popup.html @@ -8,7 +8,7 @@
-

bulk-delete-chatGPT

+

ChatGPT Bulk Delete

@@ -30,8 +30,7 @@ + id="sponsorLink">  Sponsor @@ -56,7 +53,7 @@

Buy Bulk Archive

One-time payment of $0.99 USD to purchase access.

Do you want to continue?

- + + diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..3f5456c --- /dev/null +++ b/utils.js @@ -0,0 +1,14 @@ +function getUserInfo() { + return new Promise((resolve) => { + chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (userInfo) => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + resolve(null); + } else { + resolve(userInfo); + } + }); + }); +} + +window.getUserInfo = getUserInfo; From 5db7846aa17e0be5cf6aeb36539191912c9068bc Mon Sep 17 00:00:00 2001 From: qcrao Date: Thu, 11 Jul 2024 23:16:12 +0800 Subject: [PATCH 15/15] version to 5.2 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 3d62bff..ca9cbcd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ChatGPT Bulk Delete", - "version": "5.1", + "version": "5.2", "description": "A Chrome extension to bulk delete ChatGPT conversations", "icons": { "48": "icon48.png"