您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adiciona um Menu de Gerenciamento de Downloads para o Anitsu Cloud. Para uso no IDM desative a opção "Ver mensagem no inicio do download" e ative a opção "somente adicionar arquivos a fila".
// ==UserScript== // @name Anitsu Downloader Menu // @namespace http://tampermonkey.net/ // @version 2.3 // @description Adiciona um Menu de Gerenciamento de Downloads para o Anitsu Cloud. Para uso no IDM desative a opção "Ver mensagem no inicio do download" e ative a opção "somente adicionar arquivos a fila". // @author Jack/Kingvegeta // @match https://cloud.anitsu.moe/nextcloud/s* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ===== CONFIGURAÇÕES PERSONALIZÁVEIS ===== // Timing e Performance const SCROLL_DELAY = 2000; // Tempo entre scrolls para carregar arquivos (ms) const ITEM_PROCESSING_DELAY = 2000; // Tempo entre downloads de cada arquivo (ms) const CLICK_DELAY = 500; // Tempo de espera após cliques (ms) const MAX_SCROLL_ATTEMPTS = 20; // Máximo de tentativas de scroll const MINIMUM_FILE_COUNT = 10; // Mínimo de arquivos para considerar carregamento completo const PROGRESS_UPDATE_INTERVAL = 500; // Intervalo de atualização do contador (ms) const PROGRESS_AUTO_CLOSE_DELAY = 3000; // Tempo para fechar tela de progresso (ms) // Interface e Cores const COLORS = { primary: '#007bff', primaryDark: '#003366', secondary: '#00c6ff', success: '#28a745', successLight: '#20c997', danger: '#dc3545', warning: '#ffc107', gray: '#6c757d', lightGray: '#e9ecef', darkGray: '#495057', white: '#fff', background: '#f8f9fa', border: '#dee2e6' }; // Tamanhos e Dimensões const SIZES = { buttonPadding: '10px 32px', popupPadding: '40px', contentPadding: '24px', borderRadius: '16px', buttonBorderRadius: '32px', minPopupWidth: '500px', maxPopupWidth: '90vw', maxPopupHeight: '85vh', progressBarHeight: '20px', buttonMinWidth: '180px' }; // Fontes e Texto const FONTS = { primary: 'Segoe UI, sans-serif', title: 'Montserrat, Arial Black, sans-serif' }; const FONT_SIZES = { buttonText: '18px', popupTitle: '26px', contentTitle: '20px', progressTitle: '24px', regular: '16px', small: '14px', closeButton: '18px' }; // Z-Index Layers const Z_INDEX = { mainButton: 9998, overlay: 9999, popup: 10000, popupTitle: 10002, closeButton: 10003, progressOverlay: 10100 }; // Textos e Labels const TEXTS = { mainButton: 'Anitsu Downloader Menu', popupTitle: 'Anitsu Downloader Menu', loadingTitle: 'Listando arquivos, aguarde...', progressTitle: 'Processando Downloads', selectAll: 'Selecionar Todos', downloadSelected: 'Download Selected', downloadAll: 'Download All', processing: 'Processando...', filesFound: 'arquivos encontrados', filesFoundCounter: 'Arquivos encontrados:', noFilesFound: 'Nenhum arquivo encontrado.', selectAtLeastOne: 'Selecione pelo menos um arquivo para baixar!', confirmDownloadAll: '\nTem certeza que deseja baixar TODOS os {count} arquivos encontrados?', processingFile: 'Processando:', filesProcessed: 'arquivos processados', completed: '✅ Concluído! {count} arquivos enviados para download', noNewFiles: 'Nenhum arquivo novo para processar.', progressInit: 'Iniciando...', noFunction: 'Função showPopup não está definida!', fileDefaultName: 'Arquivo' }; // Seletores CSS const SELECTOR_FILE_ROW = 'tr[data-file]'; const SELECTOR_MENU_BUTTON = 'a.action-menu'; const SELECTOR_DOWNLOAD_LINK = 'a.menuitem.action.action-download'; // ===== CÓDIGO PRINCIPAL ===== let processedFiles = new Set(); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function scrollToBottom() { let scrollAttempts = 0; let previousHeight = document.documentElement.scrollHeight; let initialItemsCount = document.querySelectorAll(SELECTOR_FILE_ROW).length; while (scrollAttempts < MAX_SCROLL_ATTEMPTS) { window.scrollTo(0, document.body.scrollHeight); await sleep(SCROLL_DELAY); let currentHeight = document.documentElement.scrollHeight; let currentItemsCount = document.querySelectorAll(SELECTOR_FILE_ROW).length; if (currentHeight === previousHeight && currentItemsCount === initialItemsCount) { break; } previousHeight = currentHeight; initialItemsCount = currentItemsCount; scrollAttempts++; } } // Função para processar arquivos com atualização periódica da barra de progresso async function processFiles(selectedFiles) { console.log("[Nextcloud DL] Iniciando processo de download..."); await scrollToBottom(); await sleep(2000); const fileRows = document.querySelectorAll(SELECTOR_FILE_ROW); if (fileRows.length === 0) { alert("Nenhum arquivo encontrado."); return; } // Filtra apenas arquivos (com extensão) const filesToProcess = Array.from(fileRows).filter(row => { const extensionElement = row.querySelector('.extension'); if (!extensionElement) return false; // Pula pastas const fileNameElement = row.querySelector('.innernametext'); const fileName = fileNameElement ? fileNameElement.innerText.trim() : 'Unknown File'; const fileExtension = extensionElement ? extensionElement.innerText.trim() : ''; const fullFileName = fileName + fileExtension; if (selectedFiles && !selectedFiles.includes(fullFileName)) return false; if (processedFiles.has(fullFileName)) return false; return true; }); if (filesToProcess.length === 0) { alert("Nenhum arquivo novo para processar."); return; } // Cria tela de progresso const progressOverlay = createProgressScreen(filesToProcess.length); document.body.appendChild(progressOverlay); let processedCount = 0; let currentFile = ''; let intervalId = setInterval(() => { updateProgressScreen(progressOverlay, processedCount, filesToProcess.length, currentFile); }, PROGRESS_UPDATE_INTERVAL); for (const row of filesToProcess) { const fileNameElement = row.querySelector('.innernametext'); const extensionElement = row.querySelector('.extension'); const fileName = fileNameElement ? fileNameElement.innerText.trim() : 'Unknown File'; const fileExtension = extensionElement ? extensionElement.innerText.trim() : ''; const fullFileName = fileName + fileExtension; processedCount++; currentFile = fullFileName; const menuButton = row.querySelector(SELECTOR_MENU_BUTTON); if (menuButton) { menuButton.click(); await sleep(CLICK_DELAY); const downloadLink = document.querySelector(SELECTOR_DOWNLOAD_LINK); if (downloadLink) { downloadLink.click(); processedFiles.add(fullFileName); } document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, which: 27, bubbles: true })); await sleep(CLICK_DELAY); } await sleep(ITEM_PROCESSING_DELAY); } clearInterval(intervalId); updateProgressScreen(progressOverlay, processedCount, filesToProcess.length, currentFile); finalizeProgressScreen(progressOverlay, processedCount); setTimeout(() => { if (progressOverlay && progressOverlay.parentNode) { progressOverlay.remove(); } }, PROGRESS_AUTO_CLOSE_DELAY); } // Função para criar tela de progresso function createProgressScreen(totalFiles) { const overlay = document.createElement('div'); overlay.id = 'anitsu-progress-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.85)', zIndex: Z_INDEX.progressOverlay, display: 'flex', alignItems: 'center', justifyContent: 'center' }); const progressPopup = document.createElement('div'); Object.assign(progressPopup.style, { background: COLORS.white, borderRadius: SIZES.borderRadius, boxShadow: '0 8px 32px rgba(0,0,0,0.25)', padding: SIZES.popupPadding, textAlign: 'center', minWidth: '400px', maxWidth: SIZES.maxPopupWidth }); // Título const title = document.createElement('div'); title.textContent = TEXTS.progressTitle; Object.assign(title.style, { fontSize: FONT_SIZES.progressTitle, fontWeight: 'bold', fontFamily: FONTS.title, color: COLORS.primary, textShadow: '2px 2px 0px rgba(0,0,0,0.8), -1px -1px 0px rgba(0,0,0,0.8), 1px -1px 0px rgba(0,0,0,0.8), -1px 1px 0px rgba(0,0,0,0.8)', marginBottom: '20px' }); // Status atual const status = document.createElement('div'); status.textContent = TEXTS.progressInit; status.id = 'progress-status'; Object.assign(status.style, { fontSize: FONT_SIZES.regular, color: COLORS.darkGray, marginBottom: '20px', minHeight: '20px' }); // Progress bar container const progressContainer = document.createElement('div'); Object.assign(progressContainer.style, { width: '100%', height: SIZES.progressBarHeight, background: COLORS.lightGray, borderRadius: '10px', overflow: 'hidden', marginBottom: '15px', boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.1)' }); // Progress bar const progressBar = document.createElement('div'); progressBar.id = 'progress-bar'; Object.assign(progressBar.style, { width: '0%', height: '100%', background: `linear-gradient(90deg, ${COLORS.primary} 0%, ${COLORS.secondary} 100%)`, borderRadius: '10px', transition: 'width 0.3s ease', position: 'relative' }); // Progress text const progressText = document.createElement('div'); progressText.id = 'progress-text'; progressText.textContent = '0%'; Object.assign(progressText.style, { fontSize: FONT_SIZES.small, fontWeight: 'bold', color: COLORS.darkGray, marginTop: '10px' }); // Contador de arquivos const counter = document.createElement('div'); counter.id = 'progress-counter'; counter.textContent = `0 de ${totalFiles} ${TEXTS.filesProcessed}`; Object.assign(counter.style, { fontSize: FONT_SIZES.small, color: COLORS.gray, marginTop: '10px' }); progressContainer.appendChild(progressBar); progressPopup.appendChild(title); progressPopup.appendChild(status); progressPopup.appendChild(progressContainer); progressPopup.appendChild(progressText); progressPopup.appendChild(counter); overlay.appendChild(progressPopup); return overlay; } // Função para atualizar progresso (agora usando PROGRESS_UPDATE_INTERVAL) function updateProgressScreen(overlay, current, total, currentFile) { const percentage = Math.round((current / total) * 100); const status = overlay.querySelector('#progress-status'); const progressBar = overlay.querySelector('#progress-bar'); const progressText = overlay.querySelector('#progress-text'); const counter = overlay.querySelector('#progress-counter'); if (status) status.textContent = `${TEXTS.processingFile} ${currentFile}`; if (progressBar) progressBar.style.width = `${percentage}%`; if (progressText) progressText.textContent = `${percentage}%`; if (counter) counter.textContent = `${current} de ${total} ${TEXTS.filesProcessed}`; } // Função para finalizar progresso (usando PROGRESS_AUTO_CLOSE_DELAY) function finalizeProgressScreen(overlay, totalProcessed) { const status = overlay.querySelector('#progress-status'); const progressBar = overlay.querySelector('#progress-bar'); const progressText = overlay.querySelector('#progress-text'); if (status) { status.textContent = TEXTS.completed.replace('{count}', totalProcessed); status.style.color = COLORS.success; status.style.fontWeight = 'bold'; } if (progressBar) { progressBar.style.background = `linear-gradient(90deg, ${COLORS.success} 0%, ${COLORS.successLight} 100%)`; progressBar.style.width = '100%'; } if (progressText) { progressText.textContent = '100%'; progressText.style.color = COLORS.success; } } function createCompactDownloadButton() { // Botão fixo no topo, encostado no limite superior da tela const assistBtn = document.createElement('button'); assistBtn.textContent = TEXTS.mainButton; Object.assign(assistBtn.style, { position: 'fixed', top: '0px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(0, 51, 102, 0.85)', // Azul escuro transparente color: '#fff', border: '2px solid #007bff', borderRadius: '32px', // Mais arredondado fontSize: '18px', fontWeight: 'bold', fontFamily: 'Segoe UI, sans-serif', cursor: 'pointer', padding: '10px 32px', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', zIndex: '9998' // Menor que o overlay e popup }); assistBtn.addEventListener('mouseenter', () => { assistBtn.style.opacity = '0.85'; }); assistBtn.addEventListener('mouseleave', () => { assistBtn.style.opacity = '1'; }); document.body.appendChild(assistBtn); // Ao clicar no botão, mostra popup assistBtn.addEventListener('click', () => { if (typeof showPopup === 'function') { showPopup(); } else { alert(TEXTS.noFunction); } }); } // Função para carregar todos os arquivos fazendo scroll async function loadAllFiles() { let lastFileCount = 0; let currentFileCount = document.querySelectorAll(SELECTOR_FILE_ROW).length; let attempts = 0; const maxAttempts = 50; // Reduzido para ser mais eficiente console.log("[Nextcloud DL] Iniciando carregamento de todos os arquivos..."); // Encontra o container de arquivos correto - prioriza #app-content const filesContainer = document.querySelector('#app-content') || document.querySelector('.files-fileList') || document.querySelector('tbody.files-fileList') || document.querySelector('#app-content-files') || document.body; console.log("[Nextcloud DL] Container encontrado:", filesContainer.id || filesContainer.className || 'body'); // Se já temos menos de 12 arquivos, provavelmente não há mais para carregar if (currentFileCount > 0 && currentFileCount < MINIMUM_FILE_COUNT) { console.log(`[Nextcloud DL] Poucos arquivos encontrados (${currentFileCount}), assumindo que é o total`); return currentFileCount; } while (attempts < maxAttempts) { // Scroll no container específico (prioriza #app-content) if (filesContainer.id === 'app-content') { filesContainer.scrollTop = filesContainer.scrollHeight; } else if (filesContainer !== document.body) { filesContainer.scrollTop = filesContainer.scrollHeight; } // Também faz scroll na janela principal como backup window.scrollTo(0, document.documentElement.scrollHeight); // Tempo reduzido para ser mais rápido await sleep(1000); // Conta quantos arquivos temos agora currentFileCount = document.querySelectorAll(SELECTOR_FILE_ROW).length; console.log(`[Nextcloud DL] Arquivos encontrados: ${currentFileCount} (tentativa ${attempts + 1})`); // Se o número de arquivos não mudou por algumas tentativas consecutivas if (currentFileCount === lastFileCount) { attempts++; // Reduzido para 3 tentativas se não há mudança if (attempts >= 3) { console.log("[Nextcloud DL] Parando - nenhum arquivo novo encontrado por 3 tentativas"); break; } } else { attempts = 0; // Reset contador se encontrou novos arquivos console.log(`[Nextcloud DL] Novos arquivos encontrados! Total: ${currentFileCount}`); } // Se carregamos exatamente múltiplos de 18, provavelmente há mais // Mas se não é múltiplo de 18, provavelmente chegamos ao fim if (currentFileCount > lastFileCount && currentFileCount % 18 !== 0 && currentFileCount > 18) { console.log(`[Nextcloud DL] Número de arquivos (${currentFileCount}) não é múltiplo de 18, provavelmente chegamos ao fim`); break; } lastFileCount = currentFileCount; } // Volta ao topo if (filesContainer.id === 'app-content') { filesContainer.scrollTop = 0; } else if (filesContainer !== document.body) { filesContainer.scrollTop = 0; } window.scrollTo(0, 0); console.log(`[Nextcloud DL] Carregamento concluído. Total final: ${currentFileCount} arquivos`); return currentFileCount; } // Função melhorada para atualizar o popup de carregamento com progresso async function showPopup() { // Remove popups antigos const oldPopup = document.getElementById('anitsu-dl-popup'); if (oldPopup) oldPopup.remove(); const oldOverlay = document.getElementById('anitsu-dl-overlay'); if (oldOverlay) oldOverlay.remove(); // Overlay escuro const overlay = document.createElement('div'); overlay.id = 'anitsu-dl-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.92)', zIndex: '9999' }); // Fechar popup ao clicar fora overlay.addEventListener('click', (e) => { if (e.target === overlay) { popup.remove(); overlay.remove(); } }); document.body.appendChild(overlay); // Popup de carregamento const loadingPopup = document.createElement('div'); loadingPopup.id = 'anitsu-dl-loading-popup'; Object.assign(loadingPopup.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: '#fff', borderRadius: '16px', boxShadow: '0 8px 32px rgba(0,0,0,0.25)', zIndex: '10000', padding: '40px', textAlign: 'center', minWidth: '350px' }); // Título de carregamento const loadingTitle = document.createElement('div'); loadingTitle.textContent = TEXTS.loadingTitle; Object.assign(loadingTitle.style, { fontSize: '20px', fontWeight: 'bold', fontFamily: 'Montserrat, Arial Black, sans-serif', color: COLORS.primary, textShadow: '2px 2px 0px rgba(0,0,0,0.8), -1px -1px 0px rgba(0,0,0,0.8), 1px -1px 0px rgba(0,0,0,0.8), -1px 1px 0px rgba(0,0,0,0.8)', marginBottom: '20px' }); // Contador de arquivos em tempo real const fileCounter = document.createElement('div'); fileCounter.textContent = `${TEXTS.filesFoundCounter} 0`; Object.assign(fileCounter.style, { fontSize: '16px', color: '#666', marginBottom: '20px' }); // Spinner de carregamento const spinner = document.createElement('div'); spinner.innerHTML = '⏳'; // Ícone pode ficar fixo // Adiciona animação CSS para o spinner const style = document.createElement('style'); style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); loadingPopup.appendChild(loadingTitle); loadingPopup.appendChild(fileCounter); loadingPopup.appendChild(spinner); document.body.appendChild(loadingPopup); // Monitora o progresso do carregamento const progressInterval = setInterval(() => { const currentCount = document.querySelectorAll(SELECTOR_FILE_ROW).length; fileCounter.textContent = `Arquivos encontrados: ${currentCount}`; }, 500); // Carrega todos os arquivos const totalFiles = await loadAllFiles(); // Conta apenas arquivos (com extensão), não pastas const actualFileRows = document.querySelectorAll(SELECTOR_FILE_ROW); const filesOnly = Array.from(actualFileRows).filter(row => row.querySelector('.extension')); const actualFileCount = filesOnly.length; // Para o monitoramento clearInterval(progressInterval); // Remove popup de carregamento loadingPopup.remove(); // Popup principal const popup = document.createElement('div'); popup.id = 'anitsu-dl-popup'; Object.assign(popup.style, { position: 'fixed', top: '3%', left: '50%', transform: 'translate(-50%, 0)', background: '#fff', borderRadius: '16px', boxShadow: '0 8px 32px rgba(0,0,0,0.25)', zIndex: '10000', padding: '0', minWidth: '500px', maxWidth: '90vw', maxHeight: '85vh', overflowY: 'auto', overflowX: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center' }); // Título do popup const title = document.createElement('div'); title.textContent = TEXTS.popupTitle; Object.assign(title.style, { fontSize: '26px', fontWeight: 'bold', fontFamily: 'Montserrat, Arial Black, sans-serif', color: '#fff', textShadow: '3px 3px 0px rgba(0,0,0,1), -1px -1px 0px rgba(0,0,0,1), 1px -1px 0px rgba(0,0,0,1), -1px 1px 0px rgba(0,0,0,1)', background: 'linear-gradient(90deg, #007bff 60%, #003366 100%)', padding: '16px 60px 16px 16px', width: '100%', textAlign: 'center', borderRadius: '16px 16px 0 0', marginBottom: '0px', letterSpacing: '2px', boxShadow: '0 2px 8px rgba(0,0,0,0.10)', border: 'none', position: 'sticky', top: '0', zIndex: '10002' }); // Botão fechar melhorado const closeBtn = document.createElement('button'); closeBtn.textContent = '✖'; Object.assign(closeBtn.style, { position: 'absolute', top: '50%', right: '40px', // Movido ainda mais para a esquerda para evitar a barra de scroll transform: 'translateY(-50%)', background: 'rgba(255,255,255,0.1)', border: '2px solid #ff0000', // Borda vermelha quadrada borderRadius: '4px', // Bordas ligeiramente arredondadas para ficar quadrada fontSize: '18px', color: '#fff', cursor: 'pointer', width: '32px', height: '32px', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: '10003', transition: 'all 0.2s ease' }); // Efeitos hover no botão X closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255,0,0,0.2)'; closeBtn.style.borderColor = '#ff4444'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255,255,255,0.1)'; closeBtn.style.borderColor = '#ff0000'; }); closeBtn.onclick = () => { popup.remove(); overlay.remove(); }; // Adiciona título e botão fechar title.appendChild(closeBtn); popup.appendChild(title); // Conteúdo do popup const content = document.createElement('div'); content.style.padding = '24px'; content.style.width = '100%'; content.style.boxSizing = 'border-box'; content.innerHTML = `<b style="font-size:20px;color:${COLORS.primary};">${TEXTS.selectAll} (${actualFileCount} ${TEXTS.filesFound})</b>`; // Lista de arquivos com checkboxes const filesDiv = document.createElement('div'); filesDiv.style.width = '100%'; filesDiv.style.marginTop = '24px'; filesDiv.style.wordBreak = 'break-word'; const fileRows = document.querySelectorAll(SELECTOR_FILE_ROW); let checkboxes = []; if (fileRows.length === 0) { filesDiv.textContent = TEXTS.noFilesFound; } else { // Caixa de seleção global const selectAllContainer = document.createElement('div'); selectAllContainer.style.display = 'flex'; selectAllContainer.style.alignItems = 'center'; selectAllContainer.style.marginBottom = '12px'; selectAllContainer.style.width = '100%'; const selectAllCheckbox = document.createElement('input'); selectAllCheckbox.type = 'checkbox'; selectAllCheckbox.id = 'anitsu-dl-select-all'; selectAllCheckbox.style.marginRight = '10px'; selectAllCheckbox.style.flexShrink = '0'; const selectAllLabel = document.createElement('label'); selectAllLabel.htmlFor = 'anitsu-dl-select-all'; selectAllLabel.textContent = TEXTS.selectAll; selectAllLabel.style.flexGrow = '1'; selectAllContainer.appendChild(selectAllCheckbox); selectAllContainer.appendChild(selectAllLabel); filesDiv.appendChild(selectAllContainer); fileRows.forEach((row, idx) => { const fileNameElement = row.querySelector('.innernametext'); const extensionElement = row.querySelector('.extension'); // Se não tem extensão, é uma pasta - pula if (!extensionElement) return; const fileName = fileNameElement ? fileNameElement.innerText.trim() : `${TEXTS.fileDefaultName} ${idx+1}`; // Corrigido const fileExtension = extensionElement ? extensionElement.innerText.trim() : ''; const fullFileName = fileName + fileExtension; const label = document.createElement('label'); label.style.display = 'flex'; label.style.alignItems = 'flex-start'; label.style.marginBottom = '8px'; label.style.cursor = 'pointer'; label.style.width = '100%'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = fullFileName; checkbox.checked = false; checkbox.style.marginRight = '10px'; checkbox.style.flexShrink = '0'; checkbox.style.marginTop = '2px'; const textSpan = document.createElement('span'); textSpan.textContent = fullFileName; textSpan.style.flexGrow = '1'; textSpan.style.wordBreak = 'break-word'; checkboxes.push(checkbox); label.appendChild(checkbox); label.appendChild(textSpan); filesDiv.appendChild(label); }); // Sincroniza seleção global com as individuais selectAllCheckbox.addEventListener('change', () => { checkboxes.forEach(cb => cb.checked = selectAllCheckbox.checked); }); checkboxes.forEach(cb => { cb.addEventListener('change', () => { selectAllCheckbox.checked = checkboxes.length > 0 && checkboxes.every(c => c.checked); }); }); } // Botões de ação - FIXOS NA PARTE INFERIOR const actionsDiv = document.createElement('div'); Object.assign(actionsDiv.style, { display: 'flex', gap: '16px', padding: '20px 24px', flexWrap: 'wrap', justifyContent: 'center', background: '#f8f9fa', borderTop: '1px solid #dee2e6', borderRadius: '0 0 16px 16px', position: 'sticky', bottom: '0', zIndex: '10002', boxShadow: '0 -2px 8px rgba(0,0,0,0.10)', width: '100%', boxSizing: 'border-box' }); // Download Selected const btnSelected = document.createElement('button'); btnSelected.innerHTML = `<span style="font-size:18px;vertical-align:middle;">↓</span> ${TEXTS.downloadSelected}`; Object.assign(btnSelected.style, { background: 'linear-gradient(90deg, #007bff 60%, #00c6ff 100%)', color: '#fff', border: 'none', borderRadius: '32px', fontSize: '16px', fontWeight: 'bold', fontFamily: 'Segoe UI, sans-serif', cursor: 'pointer', padding: '12px 28px', boxShadow: '0 4px 12px rgba(0,123,255,0.25)', minWidth: '180px', transition: 'all 0.2s ease' }); // Efeitos hover no botão Selected btnSelected.addEventListener('mouseenter', () => { btnSelected.style.transform = 'translateY(-2px)'; btnSelected.style.boxShadow = '0 6px 16px rgba(0,123,255,0.35)'; }); btnSelected.addEventListener('mouseleave', () => { btnSelected.style.transform = 'translateY(0)'; btnSelected.style.boxShadow = '0 4px 12px rgba(0,123,255,0.25)'; }); // Download All const btnAll = document.createElement('button'); btnAll.innerHTML = `<span style="font-size:18px;vertical-align:middle;">↓</span> ${TEXTS.downloadAll}`; Object.assign(btnAll.style, { background: 'linear-gradient(90deg, #28a745 60%, #20c997 100%)', color: '#fff', border: 'none', borderRadius: '32px', fontSize: '16px', fontWeight: 'bold', fontFamily: 'Segoe UI, sans-serif', cursor: 'pointer', padding: '12px 28px', boxShadow: '0 4px 12px rgba(40,167,69,0.25)', minWidth: '180px', transition: 'all 0.2s ease' }); // Efeitos hover no botão All btnAll.addEventListener('mouseenter', () => { btnAll.style.transform = 'translateY(-2px)'; btnAll.style.boxShadow = '0 6px 16px rgba(40,167,69,0.35)'; }); btnAll.addEventListener('mouseleave', () => { btnAll.style.transform = 'translateY(0)'; btnAll.style.boxShadow = '0 4px 12px rgba(40,167,69,0.25)'; }); // Função para baixar todos btnAll.onclick = async () => { const confirmed = confirm(TEXTS.confirmDownloadAll.replace('{count}', actualFileCount)); if (!confirmed) { return; } btnAll.disabled = true; btnSelected.disabled = true; btnAll.innerHTML = `<span style="font-size:18px;vertical-align:middle;">⌛</span> ${TEXTS.processing}`; btnAll.style.background = COLORS.gray; await processFiles(); btnAll.innerHTML = `<span style="font-size:18px;vertical-align:middle;">↓</span> ${TEXTS.downloadAll}`; btnAll.style.background = 'linear-gradient(90deg, #28a745 60%, #20c997 100%)'; btnAll.disabled = false; btnSelected.disabled = false; }; // Função para baixar selecionados btnSelected.onclick = async () => { const selectedNames = checkboxes.filter(cb => cb.checked).map(cb => cb.value); if (selectedNames.length === 0) { alert(TEXTS.selectAtLeastOne); return; } btnAll.disabled = true; btnSelected.disabled = true; btnSelected.innerHTML = `<span style="font-size:18px;vertical-align:middle;">⌛</span> ${TEXTS.processing}`; btnSelected.style.background = COLORS.gray; await processFiles(selectedNames); btnSelected.innerHTML = `<span style="font-size:18px;vertical-align:middle;">↓</span> ${TEXTS.downloadSelected}`; btnSelected.style.background = 'linear-gradient(90deg, #007bff 60%, #00c6ff 100%)'; btnSelected.disabled = false; btnAll.disabled = false; }; // Adiciona os botões actionsDiv.appendChild(btnSelected); actionsDiv.appendChild(btnAll); // Remove o marginTop dos botões do conteúdo e adiciona o actionsDiv diretamente ao popup content.appendChild(filesDiv); popup.appendChild(content); popup.appendChild(actionsDiv); // Botões fixos na parte inferior document.body.appendChild(popup); } // Espera a página estar carregada para injetar o botão const observer = new MutationObserver((mutations, obs) => { if (document.querySelector(SELECTOR_FILE_ROW)) { obs.disconnect(); createCompactDownloadButton(); console.log("[Nextcloud DL] Botão compacto inserido."); } }); observer.observe(document.body, { childList: true, subtree: true }); })(); // ===== Seleção por intervalo usando Ctrl ===== let lastCheckedIndex = null; document.addEventListener('click', function(e) { if (e.target && e.target.type === 'checkbox') { const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const currentIndex = checkboxes.indexOf(e.target); if (e.ctrlKey && lastCheckedIndex !== null) { const [start, end] = [lastCheckedIndex, currentIndex].sort((a, b) => a - b); const shouldCheck = e.target.checked; for (let i = start; i <= end; i++) { checkboxes[i].checked = shouldCheck; } } lastCheckedIndex = currentIndex; } });