diff --git a/addCheckboxes.js b/addCheckboxes.js index bb02a3b..42917d1 100644 --- a/addCheckboxes.js +++ b/addCheckboxes.js @@ -1,122 +1,152 @@ -console.log('addCheckboxes.js loaded'); +console.log("addCheckboxes.js loaded"); -// 查找带有特定选择器的祖先元素 -function findAncestorWithCheckbox(el, selector) { - while ((el = el.parentElement) && !el.querySelector(selector)); - return el; +// 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; } -// 切换复选框的选中状态 -function toggleCheckbox(event) { - // 阻止事件的默认行为(例如链接跳转) - event.preventDefault(); - // 阻止事件冒泡到父元素 +// 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"; +} - const parentElement = findAncestorWithCheckbox(event.currentTarget, `.${CHECKBOX_CLASS}`); - const checkbox = parentElement ? parentElement.querySelector(`.${CHECKBOX_CLASS}`) : null; - if (checkbox) { - checkbox.checked = !checkbox.checked; - checkPreviousCheckboxes(checkbox); +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}` + ); + const checkbox = parentElement + ? parentElement.querySelector(`.${CHECKBOX_CLASS}`) + : null; + if (checkbox) { + checkbox.checked = !checkbox.checked; + checkPreviousCheckboxes(checkbox); + + // 更新最后选中的复选框 + if (checkbox.checked) { + window.lastCheckedCheckbox = checkbox; } - event.stopPropagation(); + } } function handleCheckboxClick(event) { - // 阻止事件冒泡 - event.stopPropagation(); + event.stopPropagation(); - checkPreviousCheckboxes(event.target); + const clickedCheckbox = event.target; + checkPreviousCheckboxes(clickedCheckbox); + + // 更新最后选中的复选框 + window.lastCheckedCheckbox = clickedCheckbox; } function checkPreviousCheckboxes(clickedCheckbox) { - if (shiftPressed && clickedCheckbox.checked) { - const allCheckboxes = Array.from(document.querySelectorAll(`.${CHECKBOX_CLASS}`)); - const previousCheckboxes = allCheckboxes.slice(0, allCheckboxes.indexOf(clickedCheckbox)); + if (window.shiftPressed && window.lastCheckedCheckbox) { + const allCheckboxes = Array.from( + document.querySelectorAll(`.${CHECKBOX_CLASS}`) + ); + const start = allCheckboxes.indexOf(window.lastCheckedCheckbox); + const end = allCheckboxes.indexOf(clickedCheckbox); - let index = previousCheckboxes.length - 1; - while (index >= 0 && !previousCheckboxes[index].checked) { - index--; - } + const [lower, upper] = start < end ? [start, end] : [end, start]; - // A negative index means no previous checkbox is checked - if (index >= 0) { - previousCheckboxes.slice(index).forEach((checkbox) => { - checkbox.checked = true; - }); - } + for (let i = lower; i <= upper; i++) { + allCheckboxes[i].checked = true; } + } } function addShiftKeyEventListeners() { - console.log('Adding Shift key event listeners...'); - document.addEventListener('keydown', (event) => { - if (event.key === 'Shift') { - shiftPressed = true; - } - }); + console.log("Adding Shift key event listeners..."); + document.addEventListener("keydown", (event) => { + if (event.key === "Shift") { + console.log("Shift key pressed"); + window.shiftPressed = true; + } + }); - document.addEventListener('keyup', (event) => { - if (event.key === 'Shift') { - shiftPressed = false; - } - }); + document.addEventListener("keyup", (event) => { + if (event.key === "Shift") { + console.log("Shift key released"); + window.shiftPressed = false; + } + }); } // 添加复选框到每个对话 function addCheckboxes() { - console.log('Adding checkboxes to conversations...', Selectors); - const conversations = document.querySelectorAll(Selectors.CONVERSATION_SELECTOR); + console.log("Adding checkboxes to conversations...", Selectors); + const conversations = document.querySelectorAll( + Selectors.CONVERSATION_SELECTOR + ); - conversations.forEach((conversation, index) => { - let existingCheckbox = conversation.querySelector(`.${CHECKBOX_CLASS}`); + conversations.forEach((conversation, index) => { + let existingCheckbox = conversation.querySelector(`.${CHECKBOX_CLASS}`); - // 如果复选框已存在,获取其选中状态并移除它 - let isChecked = existingCheckbox ? existingCheckbox.checked : false; - if (existingCheckbox) { - existingCheckbox.remove(); - } + // 如果复选框已存在,获取其选中状态并移除它 + 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', handleCheckboxClick); - conversation.insertAdjacentElement('afterbegin', checkbox); + // 创建新的复选框并设置其属性 + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.className = CHECKBOX_CLASS; + checkbox.dataset.index = index; + checkbox.checked = isChecked; + checkbox.addEventListener("click", handleCheckboxClick); + conversation.insertAdjacentElement("afterbegin", checkbox); - // 为对话标题添加点击事件 - const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR); - if (titleElement) { - titleElement.style.cursor = 'default'; + // add click event listener to the title element + const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR); + if (titleElement) { + titleElement.style.cursor = "default"; - // 获取 titleElement 的父元素 - const parentElement = titleElement.parentElement; + // get the parent element of titleElement + const parentElement = titleElement.parentElement; - // 定义一个通用的事件处理函数 - const handleTitleClick = (event) => { - toggleCheckbox(event); - event.stopPropagation(); // 防止事件冒泡 - }; + // define a common event handler + const handleTitleClick = (event) => { + toggleCheckbox(event); + event.stopPropagation(); // prevent event propagation + }; - // 为 titleElement 添加点击事件 - if (!titleElement.dataset.hasClickListener) { - titleElement.addEventListener('click', handleTitleClick); - titleElement.dataset.hasClickListener = 'true'; - } + // add click event listener to 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'; - } - } - }); + // add click event listener to the parent element of titleElement + if (parentElement && !parentElement.dataset.hasClickListener) { + parentElement.style.cursor = "default"; + parentElement.addEventListener("click", handleTitleClick); + parentElement.dataset.hasClickListener = "true"; + } + } + }); - addShiftKeyEventListeners(); + addShiftKeyEventListeners(); } -// 执行主函数 +// run the main function addCheckboxes(); diff --git a/background.js b/background.js new file mode 100644 index 0000000..ed79c37 --- /dev/null +++ b/background.js @@ -0,0 +1,21 @@ +console.log("Background script loaded"); + +chrome.runtime.onInstalled.addListener(() => { + console.log("Extension installed"); +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "getUserInfo") { + chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (userInfo) => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + sendResponse({ error: chrome.runtime.lastError.message }); + } else { + sendResponse({ userInfo: userInfo }); + } + }); + return true; // Will respond asynchronously + } +}); + +console.log("Background script setup complete"); diff --git a/bulkArchiveConversations.js b/bulkArchiveConversations.js new file mode 100644 index 0000000..292e58f --- /dev/null +++ b/bulkArchiveConversations.js @@ -0,0 +1,106 @@ +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 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.textContent.trim() === textContentSimplifiedChinese || + el.textContent.trim() === textContentTraditionalChinese + ); + 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/bulkDeleteConversations.js b/bulkDeleteConversations.js index 92b9b96..1ff48b2 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(); @@ -11,6 +11,8 @@ async function bulkDeleteConversations() { console.log("Selected Conversations:", selectedConversations); + sendEventAsync(selectedConversations.length); + for (const element of selectedConversations) { await deleteConversation(element); } @@ -21,38 +23,36 @@ function getSelectedConversations() { } function removeAllCheckboxes() { - const allCheckboxes = document.querySelectorAll(Selectors.conversationsCheckbox); - allCheckboxes.forEach(checkbox => checkbox.remove()); + 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 hoverEvent = new MouseEvent('mouseover', { + const conversationElement = checkbox.parentElement; + 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); - console.log("2. Clicking three dot button...", threeDotButton); const deleteButton = await waitForDeleteButton(); @@ -60,8 +60,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,49 +74,83 @@ 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) { + 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)); + 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; // 返回找到的元素 + 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`); + + 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); } + throw new Error(`Element ${selector} did not disappear within ${timeout}ms`); } -function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); +async function sendEventAsync(count) { + try { + const userInfo = await getUserInfo(); + const timestamp = new Date().toISOString().replace("T", " ").substr(0, 19); + + const data = { + user_id: userInfo.id || "unknown", + timestamp: timestamp, + action: "delete", + count: count, + }; + + const response = await fetch( + "https://bulk-delete-chatgpt-worker.qcrao.com/send-event", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + console.log("Event sent successfully"); + } catch (error) { + console.error("Error sending event:", error); + } } bulkDeleteConversations(); diff --git a/globals.js b/globals.js index 8b1740f..d67a454 100644 --- a/globals.js +++ b/globals.js @@ -1,23 +1,39 @@ -if (typeof window.globalsLoaded === 'undefined') { - console.log('globals.js loaded'); +if (typeof window.globalsLoaded === "undefined") { + console.log("globals.js loaded"); - window.globalsLoaded = true; + window.globalsLoaded = true; - let Selectors = { - // Plus 用户的选择器 - 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', - }; + const Selectors = { + conversationsCheckbox: ".conversation-checkbox:checked", + confirmDeleteButton: "button.btn.btn-danger", + threeDotButton: '[id^="radix-"]', + 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"; - window.shiftPressed = false; - window.Selectors = Selectors; - window.CHECKBOX_CLASS = CHECKBOX_CLASS; + // Define getUserInfo function + function getUserInfo() { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ action: "getUserInfo" }, (response) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else if (response.error) { + reject(new Error(response.error)); + } else { + resolve(response.userInfo); + } + }); + }); + } + + // Expose variables to the global scope + window.Selectors = Selectors; + window.shiftPressed = false; + window.lastCheckedCheckbox = null; + window.CHECKBOX_CLASS = CHECKBOX_CLASS; } else { - console.log('globals.js already loaded, skipping re-initialization'); -} \ No newline at end of file + console.log("globals.js already loaded, skipping re-initialization"); +} diff --git a/manifest.json b/manifest.json index 313bed3..ca9cbcd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ChatGPT Bulk Delete", - "version": "4.5", + "version": "5.2", "description": "A Chrome extension to bulk delete ChatGPT conversations", "icons": { "48": "icon48.png" @@ -11,19 +11,20 @@ "default_popup": "popup.html", "default_title": "Bulk Delete Conversations" }, - "permissions": [ - "scripting", - "activeTab" - ], + "permissions": ["scripting", "activeTab", "identity", "identity.email"], + "host_permissions": ["https://bulk-delete-chatgpt-worker.qcrao.com/*"], + "background": { + "service_worker": "background.js" + }, "content_scripts": [ { - "matches": [ - "*://chat.openai.com/*" - ], + "matches": ["*://chat.openai.com/*"], "js": [ "globals.js", + "utils.js", "addCheckboxes.js", - "bulkDeleteConversations.js" + "bulkDeleteConversations.js", + "bulkArchiveConversations.js" ], "run_at": "document_idle" } diff --git a/popup.css b/popup.css index b493d13..6400c94 100644 --- a/popup.css +++ b/popup.css @@ -1,75 +1,133 @@ body { - width: 240px; - padding: 15px; - font-family: Arial, sans-serif; - display: flex; - flex-direction: column; - align-items: center; + 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 { - 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; + position: relative; +} + +button#bulk-archive span { + position: absolute; + left: 75px; + top: 50%; + transform: translateY(-50%); +} + +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 { @@ -77,41 +135,117 @@ button#bulk-delete, button#toggle-checkboxes { 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; + 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; -} \ No newline at end of file + 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 8d2cc6d..9b3f271 100644 --- a/popup.html +++ b/popup.html @@ -1,34 +1,69 @@ - - - + + + ChatGPT Bulk Delete - - - + + +
-

bulk-delete-chatGPT

+

ChatGPT Bulk Delete

-
- - -
- - -
- - - + + + + + - + + diff --git a/popup.js b/popup.js index 6d31fc6..fe1ccbe 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,17 +24,157 @@ 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"); + + const bulkArchiveButton = document.getElementById("bulk-archive"); + bulkArchiveButton.addEventListener("click", handleBulkArchive); +} + +const storageKey = "BulkDeleteChatGPT_isPaid"; +async function checkMembershipStatus() { + const localIsPaid = localStorage.getItem(storageKey) === "true"; + + updateBulkArchiveButton(localIsPaid); + + const userInfo = await getUserInfo(); + if (!userInfo) { + console.error("Unable to get user info"); + 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(); + + // 更新本地存储和按钮状态 + localStorage.setItem(storageKey, data.isPaid); + updateBulkArchiveButton(data.isPaid); + } catch (error) { + console.error("Error checking membership status:", error); + } +} + +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 localIsPaid = localStorage.getItem(storageKey) === "true"; + if (localIsPaid) { + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (tab) { + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ["bulkArchiveConversations.js"], + }); + } + }); + return; + } + + 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 { + 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."); + } + } + }); + } +} + +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 updateCopyrightYear() { const currentYear = new Date().getFullYear(); - document.getElementById('copyright').innerHTML = - `© ${currentYear} qcrao@GitHub`; + document.getElementById( + "copyright" + ).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(); + }); + } +}); 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 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;