您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adiciona botão para remover tarefas. Aumenta delay do menu 'Excluir' para mais confiabilidade.
// ==UserScript== // @name Google Tasks - Botão Remover Todas // @namespace http://tampermonkey.net/ // @version 2.3 // @description Adiciona botão para remover tarefas. Aumenta delay do menu 'Excluir' para mais confiabilidade. // @author luascfl // @match https://tasks.google.com/u/*/lists/* // @match https://tasks.google.com/embed/list/* // @icon https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Google_Tasks_2021.svg/2159px-Google_Tasks_2021.svg.png // @home https://github.com/luascfl/empty-google-tasks // @supportURL https://github.com/luascfl/empty-google-tasks/issues // @grant GM_info // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuração --- const BUTTON_ID = 'deleteAllTasksButton'; const BUTTON_TEXT = "Remover Todas as Tarefas"; const CONFIRM_MESSAGE = "Tem certeza que deseja remover TODAS as tarefas desta lista? Esta ação não pode ser desfeita."; const CHECK_INTERVAL_MS = 1000; // --- Delays Ajustados (v2.2) --- // Aumentado ACTION_DELAY_MS para dar mais tempo para o menu 'Excluir' aparecer const REVEAL_DELAY_MS = 350; // Delay APÓS clicar no título (Mantido de v2.1) const ACTION_DELAY_MS = 450; // (Era 250 em v2.1, 500 em v2.0) Delay base APÓS clicar em '...' const DELETE_MENU_APPEAR_DELAY_MS = ACTION_DELAY_MS + 50; // (Agora 500ms) Delay ANTES de procurar 'Excluir' const EVENT_SIMULATION_DELAY_MS = 30; // (Mantido de v2.1) Delay entre eventos simulados const POST_DELETE_DELAY_MS = ACTION_DELAY_MS + 150; // (Agora 600ms) Delay APÓS excluir para UI estabilizar const MAX_CONSECUTIVE_ERRORS = 5; // --- Funções Auxiliares --- const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); /** * Itera sobre as tarefas visíveis, clica no título, clica em '...', * e simula os eventos de clique em 'Excluir' (com delays ajustados). * @returns {Promise<void>} */ async function deleteAllTasks() { console.log(`[${BUTTON_ID}] Iniciando exclusão (v2.2 - Tuned Delay)...`); let tasksDeletedCount = 0; let consecutiveErrors = 0; const deleteAllButton = document.getElementById(BUTTON_ID); if (deleteAllButton) { deleteAllButton.disabled = true; deleteAllButton.textContent = "Removendo..."; } try { while (true) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { throw new Error(`Muitos erros consecutivos (${consecutiveErrors}) ao processar tarefas. Abortando.`); } // 1. Encontra a primeira tarefa let taskElement = document.querySelector('div[role="listitem"].MnEwWd') || document.querySelector('div[role="listitem"]') || document.querySelector('div.MnEwWd'); if (!taskElement) { console.log(`[${BUTTON_ID}] Nenhuma tarefa restante encontrada.`); break; } // 2. Encontra título clicável let titleContainer = taskElement.querySelector('div[jsname="GYcwYe"]'); if (!titleContainer) { const titleTextDiv = taskElement.querySelector('.JnIHn'); if (titleTextDiv) { titleContainer = titleTextDiv.closest('.lCEjKc'); } } if (!titleContainer) { titleContainer = taskElement; } if (!titleContainer) { console.error(`[${BUTTON_ID}] Erro CRÍTICO: Não encontrou elemento clicável para a tarefa. Pulando.`); consecutiveErrors++; await delay(ACTION_DELAY_MS); continue; } // 3. Clica no Título titleContainer.click(); await delay(REVEAL_DELAY_MS); // Espera painel/botão '...' // 4. Encontra "..." let moreOptionsButton = document.querySelector('button[aria-label="Opções do app Tarefas"]') || document.querySelector('button[jsname="pzCKEc"]') || document.querySelector('button[aria-label*="Opç"], button[aria-label*="Aç"], button[aria-label*="Mais"]'); if (!moreOptionsButton) { console.error(`[${BUTTON_ID}] Erro: Não encontrou '...' após ${REVEAL_DELAY_MS}ms. Aumente REVEAL_DELAY_MS? Pulando.`); consecutiveErrors++; document.body.click(); await delay(ACTION_DELAY_MS); continue; } // 5. Clica em "..." moreOptionsButton.click(); await delay(DELETE_MENU_APPEAR_DELAY_MS); // <<< DELAY AJUSTADO AQUI (500ms) // 6. Encontra e simula clique em "Excluir" const deleteButtonSelector = 'span[role="menuitem"][aria-label="Excluir"][jsname="j7LFlb"]'; const deleteButton = document.querySelector(deleteButtonSelector); if (deleteButton) { try { // Simula eventos de mouse deleteButton.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); await delay(EVENT_SIMULATION_DELAY_MS); deleteButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); await delay(EVENT_SIMULATION_DELAY_MS); deleteButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); tasksDeletedCount++; consecutiveErrors = 0; if (tasksDeletedCount % 10 === 0 || tasksDeletedCount === 1) { // Log first and every 10th console.log(`[${BUTTON_ID}] Tarefa ${tasksDeletedCount} removida.`); } await delay(POST_DELETE_DELAY_MS); // <<< DELAY AJUSTADO AQUI (600ms) } catch (e) { console.error(`[${BUTTON_ID}] Erro ao simular clique em 'Excluir':`, e); consecutiveErrors++; document.body.click(); await delay(ACTION_DELAY_MS); if (consecutiveErrors >= 2) { throw new Error("Falha crítica consecutiva ao simular clique em 'Excluir'."); } continue; } } else { // Este é o erro que ocorreu na v2.1 console.error(`[${BUTTON_ID}] Erro: Não encontrou 'Excluir' (${deleteButtonSelector}) após ${DELETE_MENU_APPEAR_DELAY_MS}ms. Aumente DELETE_MENU_APPEAR_DELAY_MS? Pulando.`); consecutiveErrors++; document.body.click(); await delay(ACTION_DELAY_MS); if (consecutiveErrors >= 2) { throw new Error("Falha crítica consecutiva ao encontrar 'Excluir'."); } continue; } } // Fim do while // Mensagem final alert(`Processo concluído!\n${tasksDeletedCount} tarefas foram removidas.`); console.log(`[${BUTTON_ID}] Processo concluído! ${tasksDeletedCount} tarefas removidas.`); } catch (error) { console.error(`[${BUTTON_ID}] Ocorreu um erro durante a exclusão:`, error); alert(`Ocorreu um erro durante a exclusão:\n${error.message}\nVerifique o console (F12) para mais detalhes.`); } finally { // Reabilitar botão if (deleteAllButton) { deleteAllButton.disabled = false; deleteAllButton.textContent = BUTTON_TEXT; deleteAllButton.style.opacity = '1'; deleteAllButton.style.cursor = 'pointer'; } console.log(`[${BUTTON_ID}] Processo de exclusão finalizado.`); } } /** * Cria e adiciona o botão "Remover Todas" à interface. * (Função mantida da v2.0 - sem alterações) * @returns {boolean} Verdadeiro se o botão foi adicionado, falso caso contrário. */ function addButton() { const addTaskButton = document.querySelector('button[jsname="MhySTb"]'); if (addTaskButton && !document.getElementById(BUTTON_ID)) { const container = addTaskButton.closest('div.bzRmOI'); const parent = container || addTaskButton.parentNode; if (!parent) { return false; } const button = document.createElement('button'); button.id = BUTTON_ID; button.textContent = BUTTON_TEXT; button.type = 'button'; // Estilos button.style.display = 'block'; button.style.width = 'calc(100% - 16px)'; button.style.boxSizing = 'border-box'; button.style.marginTop = '10px'; button.style.marginBottom = '10px'; button.style.marginLeft = '8px'; button.style.marginRight = '8px'; button.style.padding = '8px 16px'; button.style.cursor = 'pointer'; button.style.border = '1px solid #d93025'; button.style.borderRadius = '4px'; button.style.backgroundColor = '#fce8e6'; button.style.color = '#d93025'; button.style.fontWeight = '500'; button.style.fontSize = '14px'; button.style.textAlign = 'center'; button.style.lineHeight = 'normal'; // Efeitos Hover/Disabled button.onmouseover = () => { if (!button.disabled) { button.style.backgroundColor = '#fbd7d4'; button.style.borderColor = '#c5221f'; } }; button.onmouseout = () => { if (!button.disabled) { button.style.backgroundColor = '#fce8e6'; button.style.borderColor = '#d93025'; } }; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.attributeName === 'disabled') { if (button.disabled) { button.style.opacity = '0.6'; button.style.cursor = 'not-allowed'; button.style.backgroundColor = '#fce8e6'; button.style.borderColor = '#d93025'; } else { button.style.opacity = '1'; button.style.cursor = 'pointer'; } } }); }); observer.observe(button, { attributes: true }); button.addEventListener('click', (event) => { event.stopPropagation(); if (confirm(CONFIRM_MESSAGE)) { deleteAllTasks(); } else { console.log(`[${BUTTON_ID}] Exclusão cancelada pelo usuário.`); } }); parent.insertBefore(button, addTaskButton); return true; } return false; } // --- Inicialização --- console.log(`[${BUTTON_ID}] Userscript '${GM_info.script.name}' v${GM_info.script.version} iniciado. Aguardando UI...`); const checkInterval = setInterval(() => { try { if (addButton()) { clearInterval(checkInterval); } } catch (error) { console.error(`[${BUTTON_ID}] Erro durante a tentativa de adicionar o botão:`, error); } }, CHECK_INTERVAL_MS); })(); // Fim do IIFE