// ==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;
}
});