enhance shift function

This commit is contained in:
qcrao
2024-07-21 16:31:28 +08:00
12 changed files with 805 additions and 271 deletions

View File

@@ -1,122 +1,152 @@
console.log('addCheckboxes.js loaded'); console.log("addCheckboxes.js loaded");
// 查找带有特定选择器的祖先元素 // Create a new checkbox element
function findAncestorWithCheckbox(el, selector) { function createCheckbox(index) {
while ((el = el.parentElement) && !el.querySelector(selector)); const checkbox = document.createElement("input");
return el; 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 toggleCheckbox(event) { function addClickEventListener(element) {
// 阻止事件的默认行为(例如链接跳转) const handleTitleClick = (event) => {
event.preventDefault(); toggleCheckbox(event);
// 阻止事件冒泡到父元素
event.stopPropagation(); event.stopPropagation();
};
element.addEventListener("click", handleTitleClick);
element.dataset.hasClickListener = "true";
}
const parentElement = findAncestorWithCheckbox(event.currentTarget, `.${CHECKBOX_CLASS}`); function findAncestorWithCheckbox(el, selector) {
const checkbox = parentElement ? parentElement.querySelector(`.${CHECKBOX_CLASS}`) : null; while ((el = el.parentElement) && !el.querySelector(selector));
if (checkbox) { return el;
checkbox.checked = !checkbox.checked; }
checkPreviousCheckboxes(checkbox);
// 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) { function handleCheckboxClick(event) {
// 阻止事件冒泡 event.stopPropagation();
event.stopPropagation();
checkPreviousCheckboxes(event.target); const clickedCheckbox = event.target;
checkPreviousCheckboxes(clickedCheckbox);
// 更新最后选中的复选框
window.lastCheckedCheckbox = clickedCheckbox;
} }
function checkPreviousCheckboxes(clickedCheckbox) { function checkPreviousCheckboxes(clickedCheckbox) {
if (shiftPressed && clickedCheckbox.checked) { if (window.shiftPressed && window.lastCheckedCheckbox) {
const allCheckboxes = Array.from(document.querySelectorAll(`.${CHECKBOX_CLASS}`)); const allCheckboxes = Array.from(
const previousCheckboxes = allCheckboxes.slice(0, allCheckboxes.indexOf(clickedCheckbox)); document.querySelectorAll(`.${CHECKBOX_CLASS}`)
);
const start = allCheckboxes.indexOf(window.lastCheckedCheckbox);
const end = allCheckboxes.indexOf(clickedCheckbox);
let index = previousCheckboxes.length - 1; const [lower, upper] = start < end ? [start, end] : [end, start];
while (index >= 0 && !previousCheckboxes[index].checked) {
index--;
}
// A negative index means no previous checkbox is checked for (let i = lower; i <= upper; i++) {
if (index >= 0) { allCheckboxes[i].checked = true;
previousCheckboxes.slice(index).forEach((checkbox) => {
checkbox.checked = true;
});
}
} }
}
} }
function addShiftKeyEventListeners() { function addShiftKeyEventListeners() {
console.log('Adding Shift key event listeners...'); console.log("Adding Shift key event listeners...");
document.addEventListener('keydown', (event) => { document.addEventListener("keydown", (event) => {
if (event.key === 'Shift') { if (event.key === "Shift") {
shiftPressed = true; console.log("Shift key pressed");
} window.shiftPressed = true;
}); }
});
document.addEventListener('keyup', (event) => { document.addEventListener("keyup", (event) => {
if (event.key === 'Shift') { if (event.key === "Shift") {
shiftPressed = false; console.log("Shift key released");
} window.shiftPressed = false;
}); }
});
} }
// 添加复选框到每个对话 // 添加复选框到每个对话
function addCheckboxes() { function addCheckboxes() {
console.log('Adding checkboxes to conversations...', Selectors); console.log("Adding checkboxes to conversations...", Selectors);
const conversations = document.querySelectorAll(Selectors.CONVERSATION_SELECTOR); const conversations = document.querySelectorAll(
Selectors.CONVERSATION_SELECTOR
);
conversations.forEach((conversation, index) => { conversations.forEach((conversation, index) => {
let existingCheckbox = conversation.querySelector(`.${CHECKBOX_CLASS}`); let existingCheckbox = conversation.querySelector(`.${CHECKBOX_CLASS}`);
// 如果复选框已存在,获取其选中状态并移除它 // 如果复选框已存在,获取其选中状态并移除它
let isChecked = existingCheckbox ? existingCheckbox.checked : false; let isChecked = existingCheckbox ? existingCheckbox.checked : false;
if (existingCheckbox) { if (existingCheckbox) {
existingCheckbox.remove(); existingCheckbox.remove();
} }
// 创建新的复选框并设置其属性 // 创建新的复选框并设置其属性
const checkbox = document.createElement('input'); const checkbox = document.createElement("input");
checkbox.type = 'checkbox'; checkbox.type = "checkbox";
checkbox.className = CHECKBOX_CLASS; checkbox.className = CHECKBOX_CLASS;
checkbox.dataset.index = index; checkbox.dataset.index = index;
checkbox.checked = isChecked; checkbox.checked = isChecked;
checkbox.addEventListener('click', handleCheckboxClick); checkbox.addEventListener("click", handleCheckboxClick);
conversation.insertAdjacentElement('afterbegin', checkbox); conversation.insertAdjacentElement("afterbegin", checkbox);
// 为对话标题添加点击事件 // add click event listener to the title element
const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR); const titleElement = conversation.querySelector(Selectors.TITLE_SELECTOR);
if (titleElement) { if (titleElement) {
titleElement.style.cursor = 'default'; titleElement.style.cursor = "default";
// 获取 titleElement 的父元素 // get the parent element of titleElement
const parentElement = titleElement.parentElement; const parentElement = titleElement.parentElement;
// 定义一个通用的事件处理函数 // define a common event handler
const handleTitleClick = (event) => { const handleTitleClick = (event) => {
toggleCheckbox(event); toggleCheckbox(event);
event.stopPropagation(); // 防止事件冒泡 event.stopPropagation(); // prevent event propagation
}; };
// 为 titleElement 添加点击事件 // add click event listener to titleElement
if (!titleElement.dataset.hasClickListener) { if (!titleElement.dataset.hasClickListener) {
titleElement.addEventListener('click', handleTitleClick); titleElement.addEventListener("click", handleTitleClick);
titleElement.dataset.hasClickListener = 'true'; titleElement.dataset.hasClickListener = "true";
} }
// 为 titleElement 的父元素添加点击事件 // add click event listener to the parent element of titleElement
if (parentElement && !parentElement.dataset.hasClickListener) { if (parentElement && !parentElement.dataset.hasClickListener) {
parentElement.style.cursor = 'default'; parentElement.style.cursor = "default";
parentElement.addEventListener('click', handleTitleClick); parentElement.addEventListener("click", handleTitleClick);
parentElement.dataset.hasClickListener = 'true'; parentElement.dataset.hasClickListener = "true";
} }
} }
}); });
addShiftKeyEventListeners(); addShiftKeyEventListeners();
} }
// 执行主函数 // run the main function
addCheckboxes(); addCheckboxes();

21
background.js Normal file
View File

@@ -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");

106
bulkArchiveConversations.js Normal file
View File

@@ -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();

View File

@@ -1,4 +1,4 @@
console.log('bulkDeleteConversations.js loaded'); console.log("bulkDeleteConversations.js loaded");
async function bulkDeleteConversations() { async function bulkDeleteConversations() {
const selectedConversations = getSelectedConversations(); const selectedConversations = getSelectedConversations();
@@ -11,6 +11,8 @@ async function bulkDeleteConversations() {
console.log("Selected Conversations:", selectedConversations); console.log("Selected Conversations:", selectedConversations);
sendEventAsync(selectedConversations.length);
for (const element of selectedConversations) { for (const element of selectedConversations) {
await deleteConversation(element); await deleteConversation(element);
} }
@@ -21,38 +23,36 @@ function getSelectedConversations() {
} }
function removeAllCheckboxes() { function removeAllCheckboxes() {
const allCheckboxes = document.querySelectorAll(Selectors.conversationsCheckbox); const allCheckboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`);
allCheckboxes.forEach(checkbox => checkbox.remove()); allCheckboxes.forEach((checkbox) => checkbox.remove());
} }
async function deleteConversation(checkbox) { async function deleteConversation(checkbox) {
const conversationElement = checkbox.parentElement;
await delay(100); await delay(100);
// console.log("1. Clicking conversation...", conversationElement);
// conversationElement.click();
// 将 click 替换为悬停
// 创建一个鼠标悬停事件 const conversationElement = checkbox.parentElement;
const hoverEvent = new MouseEvent('mouseover', { const hoverEvent = new MouseEvent("mouseover", {
view: window, view: window,
bubbles: true, bubbles: true,
cancelable: true cancelable: true,
}); });
console.log("1. Hovering over conversation...", conversationElement); console.log("1. Hovering over conversation...", conversationElement);
// 触发鼠标悬停事件 conversationElement.dispatchEvent(hoverEvent);
conversationElement.dispatchEvent(hoverEvent);
await delay(200); await delay(200);
const pointerDownEvent = new PointerEvent('pointerdown', { const pointerDownEvent = new PointerEvent("pointerdown", {
bubbles: true, bubbles: true,
cancelable: 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); threeDotButton.dispatchEvent(pointerDownEvent);
await delay(300); await delay(300);
console.log("2. Clicking three dot button...", threeDotButton);
const deleteButton = await waitForDeleteButton(); const deleteButton = await waitForDeleteButton();
@@ -60,8 +60,8 @@ async function deleteConversation(checkbox) {
console.log("3. Clicking delete button...", deleteButton); console.log("3. Clicking delete button...", deleteButton);
deleteButton.click(); deleteButton.click();
const confirmButton = await waitForElement(Selectors.confirmDeleteButton);
const confirmButton = await waitForElement(Selectors.confirmDeleteButton);
if (confirmButton) { if (confirmButton) {
console.log("4. Clicking confirm button..."); console.log("4. Clicking confirm button...");
confirmButton.click(); confirmButton.click();
@@ -74,49 +74,83 @@ async function deleteConversation(checkbox) {
} }
async function waitForDeleteButton(parent = document, timeout = 2000) { async function waitForDeleteButton(parent = document, timeout = 2000) {
const selector = 'div[role="menuitem"]'; // 设定好选择器 const selector = 'div[role="menuitem"]';
const textContent = "Delete"; // 指定文本内容 const textContent = "Delete";
const startedAt = Date.now(); const startedAt = Date.now();
while ((Date.now() - startedAt) < timeout) { while (Date.now() - startedAt < timeout) {
const elements = parent.querySelectorAll(selector); const elements = parent.querySelectorAll(selector);
const element = Array.from(elements).find(el => el.textContent.trim() === textContent); const element = Array.from(elements).find(
if (element) return element; // 返回找到的元素 (el) =>
el.textContent.trim() === textContent ||
el.querySelector(".text-token-text-error")
);
if (element) return element;
await delay(100); await delay(100);
} }
console.log(`Delete button not found within ${timeout}ms`); return null;
throw new Error(`Delete button not found within ${timeout}ms`);
} }
// Helper function to create a delay
function delay(ms) { 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) { async function waitForElement(selector, parent = document, timeout = 2000) {
const startedAt = Date.now(); const startedAt = Date.now();
while ((Date.now() - startedAt) < timeout) { while (Date.now() - startedAt < timeout) {
const element = parent.querySelector(selector); const element = parent.querySelector(selector);
if (element) return element; // 返回找到的元素 if (element) return element;
await delay(100); 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) { async function waitForElementToDisappear(selector, timeout = 2000) {
const startedAt = Date.now(); const startedAt = Date.now();
while ((Date.now() - startedAt) < timeout) { while (Date.now() - startedAt < timeout) {
const element = document.querySelector(selector); const element = document.querySelector(selector);
if (!element) return; if (!element) return;
await delay(100); await delay(100);
} }
throw new Error(`Element ${selector} did not disappear within ${timeout}ms`); throw new Error(`Element ${selector} did not disappear within ${timeout}ms`);
} }
function delay(ms) { async function sendEventAsync(count) {
return new Promise(resolve => setTimeout(resolve, ms)); 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(); bulkDeleteConversations();

View File

@@ -1,23 +1,39 @@
if (typeof window.globalsLoaded === 'undefined') { if (typeof window.globalsLoaded === "undefined") {
console.log('globals.js loaded'); console.log("globals.js loaded");
window.globalsLoaded = true; window.globalsLoaded = true;
let Selectors = { const Selectors = {
// Plus 用户的选择器 conversationsCheckbox: ".conversation-checkbox:checked",
conversationsCheckbox: '.conversation-checkbox:checked', confirmDeleteButton: "button.btn.btn-danger",
confirmDeleteButton: 'button.btn.btn-danger', threeDotButton: '[id^="radix-"]',
threeDotButton: '[id^="radix-"]', CONVERSATION_SELECTOR:
// 其他 Plus 用户选择器... "div > div > div > div > div > div > nav > div > div > div > div > ol > li > div > a",
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",
TITLE_SELECTOR: '.relative.grow.overflow-hidden.whitespace-nowrap', };
};
let CHECKBOX_CLASS = 'conversation-checkbox'; const CHECKBOX_CLASS = "conversation-checkbox";
window.shiftPressed = false; // Define getUserInfo function
window.Selectors = Selectors; function getUserInfo() {
window.CHECKBOX_CLASS = CHECKBOX_CLASS; 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 { } else {
console.log('globals.js already loaded, skipping re-initialization'); console.log("globals.js already loaded, skipping re-initialization");
} }

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "ChatGPT Bulk Delete", "name": "ChatGPT Bulk Delete",
"version": "4.5", "version": "5.2",
"description": "A Chrome extension to bulk delete ChatGPT conversations", "description": "A Chrome extension to bulk delete ChatGPT conversations",
"icons": { "icons": {
"48": "icon48.png" "48": "icon48.png"
@@ -11,19 +11,20 @@
"default_popup": "popup.html", "default_popup": "popup.html",
"default_title": "Bulk Delete Conversations" "default_title": "Bulk Delete Conversations"
}, },
"permissions": [ "permissions": ["scripting", "activeTab", "identity", "identity.email"],
"scripting", "host_permissions": ["https://bulk-delete-chatgpt-worker.qcrao.com/*"],
"activeTab" "background": {
], "service_worker": "background.js"
},
"content_scripts": [ "content_scripts": [
{ {
"matches": [ "matches": ["*://chat.openai.com/*"],
"*://chat.openai.com/*"
],
"js": [ "js": [
"globals.js", "globals.js",
"utils.js",
"addCheckboxes.js", "addCheckboxes.js",
"bulkDeleteConversations.js" "bulkDeleteConversations.js",
"bulkArchiveConversations.js"
], ],
"run_at": "document_idle" "run_at": "document_idle"
} }

276
popup.css
View File

@@ -1,75 +1,133 @@
body { body {
width: 240px; width: 280px;
padding: 15px; padding: 20px;
font-family: Arial, sans-serif; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background-color: #f0f2f5;
color: #333;
} }
.header { .header {
margin-bottom: 10px; margin-bottom: 20px;
width: 100%; width: 100%;
border-bottom: 1px solid #ccc; border-bottom: 2px solid #e1e4e8;
padding-bottom: 10px; padding-bottom: 15px;
} }
.header h1 { .header h1 {
font-size: 16px; font-size: 18px;
font-weight: bold; font-weight: 600;
text-align: center; text-align: center;
margin: 0; margin: 0;
} color: #2c3e50;
.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 的宽度减去左右内边距再加上两者之间的距离 */
} }
.buttons-wrapper { .buttons-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; 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 { .buttons-row button {
width: 120px; /* 将 remove-checkboxes 和 add-checkboxes 按钮的宽度增加到 120px */ flex: 1;
} }
.buttons-row button:first-child { button#bulk-delete,
margin-right: 10px; /* 添加一个右外边距来增加两个按钮之间的距离 */ 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 { .footer {
@@ -77,41 +135,117 @@ button#bulk-delete, button#toggle-checkboxes {
width: 100%; width: 100%;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
border-top: 1px solid #ccc; border-top: 2px solid #e1e4e8;
padding-top: 10px; padding-top: 15px;
display: flex; /* 添加这一行使得.footer下的直接子元素并排排列 */ display: flex;
justify-content: center; /* 使得子元素在中心对齐 */ justify-content: center;
gap: 10px; /* 如果需要,可以添加这一行以在子元素之间创建一些空间 */ gap: 20px;
}
button#remove-checkboxes {
background-color: #badb34;
border-color: #b6b929;
}
button#toggle-checkboxes {
background-color: #2287da;
border-color: #2758ab;
} }
#sponsorLink { #sponsorLink {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 10px 20px; padding: 8px 15px;
background-color: #383E46; background-color: #34495e;
border-color: #78838F;
border-radius: 5px; border-radius: 5px;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
cursor: pointer; 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 { #sponsorLink .icon-text {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
} }
#sponsorLink:hover { #sponsorLink:hover {
background-color: #993366; background-color: #993366;
} 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;
}

View File

@@ -1,34 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatGPT Bulk Delete</title> <title>ChatGPT Bulk Delete</title>
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css" />
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>bulk-delete-chatGPT</h1> <h1>ChatGPT Bulk Delete</h1>
</div> </div>
<div class="buttons-wrapper"> <div class="buttons-wrapper">
<div class="buttons-row"> <div class="buttons-row">
<button id="add-checkboxes"><span>Add</span><br><span>Checkboxes</span></button> <button id="add-checkboxes">
<button id="remove-checkboxes"><span>Remove</span><br><span>Checkboxes</span></button> <span>Add</span><br /><span>Checkboxes</span>
</div> </button>
<button id="toggle-checkboxes">Toggle Checkboxes</button> <button id="remove-checkboxes">
<button id="bulk-delete">Bulk Delete</button> <span>Remove</span><br /><span>Checkboxes</span>
</div> </button>
</div>
<div class="footer"> <button id="toggle-checkboxes">Toggle Checkboxes</button>
<a href="https://github.com/sponsors/qcrao" target="_blank" id="sponsorLink"> <button id="bulk-archive" class="locked">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-heart icon-sponsor mr-1 v-align-middle color-fg-sponsors anim-pulse-in" style="fill: #BC6796; vertical-align: middle;"> <span>🔒</span> Bulk Archive
<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path> </button>
</svg> <button id="bulk-delete">Bulk Delete</button>
&nbsp;Sponsor
</a>
<p id="copyright"></p>
</div> </div>
<div class="footer">
<a
href="https://github.com/sponsors/qcrao"
target="_blank"
id="sponsorLink">
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
class="octicon octicon-heart icon-sponsor mr-1 v-align-middle color-fg-sponsors anim-pulse-in"
style="fill: #bc6796; vertical-align: middle">
<path
d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path>
</svg>
&nbsp;Sponsor
</a>
<p id="copyright"></p>
</div>
<div id="customModal" class="modal">
<div class="modal-content">
<h2>Buy Bulk Archive</h2>
<p>One-time payment of $0.99 USD to purchase access.</p>
<p>Do you want to continue?</p>
<div class="modal-separator"></div>
<div class="modal-buttons">
<button id="modalCancel" class="modal-button">Cancel</button>
<button id="modalOK" class="modal-button modal-button-primary">
OK
</button>
</div>
</div>
</div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> <script src="utils.js"></script>
</body>
</html> </html>

177
popup.js
View File

@@ -1,17 +1,20 @@
function loadGlobalsThenExecute(tabId, secondaryScript) { function loadGlobalsThenExecute(tabId, secondaryScript) {
chrome.scripting.executeScript({ chrome.scripting.executeScript(
target: { tabId: tabId }, {
files: ['globals.js']
}, () => {
chrome.scripting.executeScript({
target: { tabId: tabId }, target: { tabId: tabId },
files: [secondaryScript] files: ["globals.js"],
}); },
}); () => {
chrome.scripting.executeScript({
target: { tabId: tabId },
files: [secondaryScript],
});
}
);
} }
function addButtonListener(buttonId, scriptName) { function addButtonListener(buttonId, scriptName) {
document.getElementById(buttonId).addEventListener('click', () => { document.getElementById(buttonId).addEventListener("click", () => {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (tab) { if (tab) {
loadGlobalsThenExecute(tab.id, scriptName); loadGlobalsThenExecute(tab.id, scriptName);
@@ -21,17 +24,157 @@ function addButtonListener(buttonId, scriptName) {
} }
function initializeButtons() { function initializeButtons() {
addButtonListener('add-checkboxes', 'addCheckboxes.js'); addButtonListener("add-checkboxes", "addCheckboxes.js");
addButtonListener('bulk-delete', 'bulkDeleteConversations.js'); addButtonListener("bulk-delete", "bulkDeleteConversations.js");
addButtonListener('toggle-checkboxes', 'toggleCheckboxes.js'); addButtonListener("toggle-checkboxes", "toggleCheckboxes.js");
addButtonListener('remove-checkboxes', 'removeCheckboxes.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() { function updateCopyrightYear() {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
document.getElementById('copyright').innerHTML = document.getElementById(
`&copy; ${currentYear} <a href="https://github.com/qcrao/bulk-delete-chatGPT" target="_blank">qcrao@GitHub</a>`; "copyright"
).innerHTML = `&copy; ${currentYear} <a href="https://github.com/qcrao/bulk-delete-chatGPT" target="_blank">qcrao@GitHub</a>`;
} }
initializeButtons(); document.addEventListener("DOMContentLoaded", function () {
updateCopyrightYear(); initializeButtons();
updateCopyrightYear();
checkMembershipStatus();
});
// 每次打开popup时检查会员状态
chrome.runtime.onConnect.addListener(function (port) {
if (port.name === "popup") {
port.onDisconnect.addListener(function () {
checkMembershipStatus();
});
}
});

View File

@@ -1,9 +1,9 @@
(function() { function removeCheckboxesAndReload() {
const removeConversationCheckboxes = document.querySelectorAll('.conversation-checkbox'); const checkboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}`);
removeConversationCheckboxes.forEach(checkbox => { checkboxes.forEach(checkbox => checkbox.remove());
checkbox.remove();
}); // Refresh the page after removing all checkboxes
// 在移除所有复选框后刷新页面 location.reload();
location.reload(); }
})();
removeCheckboxesAndReload();

View File

@@ -1,9 +1,9 @@
function toggleCheckboxes() { function toggleCheckboxes() {
const conversations = document.querySelectorAll(".conversation-checkbox"); const conversations = document.querySelectorAll(`.${CHECKBOX_CLASS}`);
conversations.forEach((checkbox) => { conversations.forEach((checkbox) => {
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
}); });
} }
toggleCheckboxes(); toggleCheckboxes();

14
utils.js Normal file
View File

@@ -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;