// ==UserScript==
// @name Real-Debrid Enhancer
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Enhance Real-Debrid with clickable rows, copy and debrid buttons, grid layout, and improved layout management on torrents and downloader pages.
// @author UnderPL
// @license MIT
// @match https://real-debrid.com/torrents*
// @match https://real-debrid.com/
// @match https://real-debrid.com/downloader*
// @grant GM_setClipboard
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
let copyButton, debridButton, deleteButton;
GM_addStyle(`
/* Selection styling */
.tr.g1:not(.warning), .tr.g2:not(.warning), .tr.g1:not(.warning) + tr, .tr.g2:not(.warning) + tr {
cursor: pointer;
position: relative;
transition: all 0.2s ease-in-out;
}
.tr.g1.selected, .tr.g2.selected, .tr.g1.selected + tr, .tr.g2.selected + tr {
background-color: rgba(40, 167, 69, 0.15) !important;
border-left: 4px solid #28a745 !important;
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.1);
}
.tr.g1:hover:not(.selected):not(.warning),
.tr.g2:hover:not(.selected):not(.warning),
.tr.g1:hover:not(.selected):not(.warning) + tr,
.tr.g2:hover:not(.selected):not(.warning) + tr {
background-color: rgba(40, 167, 69, 0.05);
}
.torrent-entry {
transition: all 0.2s ease-in-out;
border: 1px solid transparent;
}
.torrent-entry.selected {
background-color: rgba(40, 167, 69, 0.15) !important;
border: 1px solid #28a745 !important;
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.1);
transform: translateY(-1px);
}
.torrent-entry:hover:not(.selected) {
background-color: rgba(40, 167, 69, 0.05);
transform: translateY(-1px);
}
.tr.g1, .tr.g2 {
border-top: 2px solid black/* Green border on top */
}
.tr.g1 + tr, .tr.g2 + tr {
border-bottom: 2px solid black; /* Green border on bottom */
}
#buttonContainer {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 9999;
}
#buttonContainer button {
padding: 12px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.3px;
transition: all 0.2s ease;
box-shadow: 0 3px 6px rgba(0,0,0,0.16);
min-width: 200px;
width: 250px; /* Fixed width for all buttons */
text-align: center;
text-transform: uppercase;
white-space: nowrap; /* Prevent text wrapping */
overflow: hidden; /* Hide overflow text */
text-overflow: ellipsis; /* Show ellipsis for overflow */
}
#buttonContainer button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
filter: brightness(1.05);
}
#buttonContainer button:active {
transform: translateY(1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Button click animation */
.button-clicked {
animation: button-click-animation 0.5s ease;
background-color: #3a8a3e !important; /* Darker shade */
}
@keyframes button-click-animation {
0% { transform: scale(1); }
50% { transform: scale(0.95); }
100% { transform: scale(1); }
}
/* Only apply grid layout when the class is present */
#facebox .content.grid-layout {
width: 90vw !important;
max-width: 1200px !important;
display: flex !important;
flex-wrap: wrap !important;
justify-content: space-between !important;
}
/* Center the facebox when grid layout is applied */
#facebox.grid-layout {
left: 50% !important;
transform: translateX(-50%) !important;
}
.torrent-info {
width: calc(33.33% - 20px);
margin-bottom: 20px;
border: 1px solid #ccc;
padding: 10px;
box-sizing: border-box;
}
#switchLayoutButton {
padding: 12px 20px !important;
background-color: #2196F3 !important;
color: white !important;
border: none !important;
border-radius: 10px !important;
cursor: pointer !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
letter-spacing: 0.3px !important;
transition: all 0.2s ease !important;
box-shadow: 0 3px 6px rgba(0,0,0,0.16) !important;
text-transform: uppercase !important;
}
#switchLayoutButton:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
filter: brightness(1.05) !important;
}
#switchLayoutButton:active {
transform: translateY(1px) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
#extractUrlsButton {
padding: 8px 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.3px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-transform: uppercase;
position: absolute;
right: 10px;
top: 10px;
}
#extractUrlsButton:hover {
transform: translateY(-1px);
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
filter: brightness(1.05);
}
#extractUrlsButton:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
`);
function initializeApplication() {
if (window.location.href.includes('/torrents')) {
cleanupTorrentPageLayout();
createFloatingButtons();
makeItemsSelectable();
updateFloatingButtonsVisibility();
setupTorrentInfoWindowObserver();
checkForTorrentInfoWindow();
setupItemHoverEffects();
movePaginationToBottomRight();
addSwitchToGridLayoutButton(); // Comment this and uncomment line below to automatically switch to the more compact version of the torrent page
//switchToGridLayout()
}
if (window.location.href === 'https://real-debrid.com/' || window.location.href.includes('/downloader')) {
addExtractUrlsButtonToDownloader();
addCopyLinksButton();
}
}
function movePaginationToBottomRight() {
const parentElement = document.querySelector('div.full_width_wrapper');
const formElement = parentElement.querySelector('form:nth-child(1)');
const pageElements = parentElement.querySelectorAll('div.full_width_wrapper > strong, div.full_width_wrapper > a[href^="./torrents?p="]');
const containerDiv = document.createElement('div');
const marginSize = '5px';
const fontSize = '16px';
containerDiv.style.position = 'absolute';
containerDiv.style.right = '0';
containerDiv.style.bottom = '0';
containerDiv.style.display = 'flex';
containerDiv.style.gap = marginSize;
containerDiv.style.fontSize = fontSize;
pageElements.forEach(page => {
containerDiv.appendChild(page);
});
formElement.style.position = 'relative';
formElement.appendChild(containerDiv);
// Add selection buttons
addSelectionButtons(formElement);
}
function addSelectionButtons(formElement) {
// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.id = 'selectionButtonsContainer';
buttonContainer.style.display = 'inline-block';
buttonContainer.style.marginLeft = '10px';
buttonContainer.style.gap = '10px';
// Create Select All button
const selectAllButton = document.createElement('button');
selectAllButton.id = 'selectAllButton';
selectAllButton.textContent = 'Select All';
selectAllButton.type = 'button'; // Prevent form submission
selectAllButton.className = 'selection-control-button';
selectAllButton.addEventListener('click', (e) => {
// Add visual feedback without text change
addButtonClickFeedback(selectAllButton);
selectAllItems();
});
// Create Unselect All button
const unselectAllButton = document.createElement('button');
unselectAllButton.id = 'unselectAllButton';
unselectAllButton.textContent = 'Unselect All';
unselectAllButton.type = 'button'; // Prevent form submission
unselectAllButton.className = 'selection-control-button';
unselectAllButton.addEventListener('click', (e) => {
// Add visual feedback without text change
addButtonClickFeedback(unselectAllButton);
unselectAllItems();
});
// Create Reverse Selection button (hidden initially using opacity instead of display:none)
const reverseSelectionButton = document.createElement('button');
reverseSelectionButton.id = 'reverseSelectionButton';
reverseSelectionButton.textContent = 'Invert Selection';
reverseSelectionButton.type = 'button'; // Prevent form submission
reverseSelectionButton.className = 'selection-control-button';
// Use opacity and pointer-events to hide rather than display:none
reverseSelectionButton.style.opacity = '0';
reverseSelectionButton.style.pointerEvents = 'none';
reverseSelectionButton.style.transition = 'opacity 0.2s ease';
reverseSelectionButton.addEventListener('click', (e) => {
// Add visual feedback without text change
addButtonClickFeedback(reverseSelectionButton);
reverseSelection();
});
// Add buttons to container
buttonContainer.appendChild(selectAllButton);
buttonContainer.appendChild(unselectAllButton);
buttonContainer.appendChild(reverseSelectionButton);
// Find the Convert button and insert our buttons after it
const convertButton = formElement.querySelector('input[value="Convert"]');
if (convertButton) {
// Insert after the Convert button
convertButton.insertAdjacentElement('afterend', buttonContainer);
} else {
// Fallback - just append to the form
formElement.appendChild(buttonContainer);
}
// Add CSS for buttons
GM_addStyle(`
.selection-control-button {
padding: 8px 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.3px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-transform: uppercase;
margin-right: 5px;
display: inline-block;
min-width: 120px; /* Minimum width for selection buttons */
text-align: center;
white-space: nowrap; /* Prevent text wrapping */
}
.selection-control-button:hover {
transform: translateY(-1px);
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
filter: brightness(1.05);
}
.selection-control-button:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.selection-control-button.button-clicked {
background-color: #1976D2 !important; /* Darker blue */
}
#selectionButtonsContainer {
vertical-align: middle;
}
`);
}
function selectAllItems() {
// Get all selectable items in current view
const gridContainer = document.getElementById('torrent-grid-container');
const isGridActive = gridContainer && gridContainer.style.display !== 'none';
if (isGridActive) {
// Select all grid items
const entries = document.querySelectorAll('.torrent-entry:not(.warning)');
entries.forEach(entry => {
if (!entry.classList.contains('selected')) {
entry.classList.add('selected');
entry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
// Get ID and sync with table view
const id = getIdentifierFromElement(entry);
if (id) {
syncTableViewSelection(id, true);
}
}
});
} else {
// Select all table rows
const rows = document.querySelectorAll('.tr.g1:not(.warning), .tr.g2:not(.warning)');
rows.forEach(row => {
if (!row.classList.contains('selected')) {
row.classList.add('selected');
const nextRow = row.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
nextRow.classList.add('selected');
}
// Get ID and sync with grid view
const id = getIdentifierFromElement(row);
if (id) {
syncSelectionState(id, true);
}
}
});
}
updateFloatingButtonsVisibility();
updateReverseSelectionButtonVisibility();
}
function unselectAllItems() {
// Unselect all items in both views
document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected').forEach(item => {
item.classList.remove('selected');
item.style.backgroundColor = '';
// For table rows, also unselect detail row
if (item.classList.contains('g1') || item.classList.contains('g2')) {
const nextRow = item.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
nextRow.classList.remove('selected');
nextRow.style.backgroundColor = '';
}
}
});
updateFloatingButtonsVisibility();
updateReverseSelectionButtonVisibility();
}
function reverseSelection() {
// Get all selectable items in current view
const gridContainer = document.getElementById('torrent-grid-container');
const isGridActive = gridContainer && gridContainer.style.display !== 'none';
if (isGridActive) {
// Reverse selection in grid view
const entries = document.querySelectorAll('.torrent-entry:not(.warning)');
entries.forEach(entry => {
const isSelected = entry.classList.contains('selected');
if (isSelected) {
// Properly remove selection styles
entry.classList.remove('selected');
entry.style.backgroundColor = '';
} else {
entry.classList.add('selected');
entry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
}
// Get ID and sync with table view
const id = getIdentifierFromElement(entry);
if (id) {
syncTableViewSelection(id, !isSelected);
}
});
} else {
// Reverse selection in table view
const rows = document.querySelectorAll('.tr.g1:not(.warning), .tr.g2:not(.warning)');
rows.forEach(row => {
const isSelected = row.classList.contains('selected');
if (isSelected) {
// Properly remove selection styles
row.classList.remove('selected');
row.style.backgroundColor = '';
const nextRow = row.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
nextRow.classList.remove('selected');
nextRow.style.backgroundColor = '';
}
} else {
row.classList.add('selected');
const nextRow = row.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
nextRow.classList.add('selected');
}
}
// Get ID and sync with grid view
const id = getIdentifierFromElement(row);
if (id) {
syncSelectionState(id, !isSelected);
}
});
}
updateFloatingButtonsVisibility();
updateReverseSelectionButtonVisibility();
}
function updateReverseSelectionButtonVisibility() {
const reverseButton = document.getElementById('reverseSelectionButton');
if (!reverseButton) return;
const hasSelectedItems = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected').length > 0;
// Use opacity instead of display to show/hide
if (hasSelectedItems) {
reverseButton.style.opacity = '1';
reverseButton.style.pointerEvents = 'auto';
} else {
reverseButton.style.opacity = '0';
reverseButton.style.pointerEvents = 'none';
}
}
function createFloatingButtons() {
const container = document.createElement('div');
container.id = 'buttonContainer';
debridButton = document.createElement('button');
debridButton.addEventListener('click', (e) => {
// Add visual feedback
addButtonClickFeedback(debridButton, 'Sent to Debrid');
sendSelectedLinksToDebrid(e);
});
copyButton = document.createElement('button');
copyButton.addEventListener('click', (e) => {
// Add visual feedback
addButtonClickFeedback(copyButton, 'Copied!');
copySelectedLinksToClipboard();
});
// Add delete button
deleteButton = document.createElement('button');
deleteButton.style.backgroundColor = '#dc3545';
deleteButton.addEventListener('click', (e) => {
addButtonClickFeedback(deleteButton);
deleteSelectedTorrents();
});
container.appendChild(debridButton);
container.appendChild(copyButton);
container.appendChild(deleteButton);
document.body.appendChild(container);
return container;
}
function updateFloatingButtonsVisibility() {
const selectedLinks = getSelectedItemLinks();
const count = selectedLinks.length;
// Get unique selected items count
const uniqueSelectedIds = getUniqueSelectedItemsCount();
const itemCount = uniqueSelectedIds.length;
if (count > 0) {
debridButton.textContent = `Debrid (${count})`;
copyButton.textContent = `Copy Selected (${count})`;
deleteButton.textContent = `Delete (${itemCount})`;
debridButton.style.display = 'block';
copyButton.style.display = 'block';
deleteButton.style.display = 'block';
} else {
debridButton.style.display = 'none';
copyButton.style.display = 'none';
deleteButton.style.display = 'none';
}
// Update visibility of Reverse Selection button
updateReverseSelectionButtonVisibility();
}
function getUniqueSelectedItemsCount() {
const uniqueIds = new Set();
const gridContainer = document.getElementById('torrent-grid-container');
const isGridActive = gridContainer && gridContainer.style.display !== 'none';
if (isGridActive) {
// Count only grid items if grid view is active
const selectedEntries = document.querySelectorAll('.torrent-entry.selected');
selectedEntries.forEach(entry => {
const id = getIdentifierFromElement(entry);
if (id) uniqueIds.add(id);
});
} else {
// Count only table rows if table view is active
const selectedRows = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected');
selectedRows.forEach(row => {
const id = getIdentifierFromElement(row);
if (id) uniqueIds.add(id);
});
}
return Array.from(uniqueIds);
}
function makeItemsSelectable() {
const rows = document.querySelectorAll('.tr.g1, .tr.g2');
rows.forEach(row => {
// Skip if already has a click handler
if (row.hasAttribute('data-has-click-handler')) return;
const warningSpan = row.querySelector('span.px10 strong');
if (!warningSpan || warningSpan.textContent !== 'Warning:') {
const nextRow = row.nextElementSibling;
// Add event stopping for delete buttons and download images
const deleteButton = row.querySelector('a[href*="del"]');
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
});
}
// Add event stopping for file info buttons
const fileInfoButton = row.querySelector('a[rel="facebox"]');
if (fileInfoButton) {
fileInfoButton.addEventListener('click', (e) => {
e.stopPropagation();
});
}
const clickHandler = () => {
row.classList.toggle('selected');
if (nextRow) {
nextRow.classList.toggle('selected');
}
// Get ID and sync with grid view
const id = getIdentifierFromElement(row);
if (id) {
syncSelectionState(id, row.classList.contains('selected'));
}
updateFloatingButtonsVisibility();
};
row.addEventListener('click', clickHandler);
row.setAttribute('data-has-click-handler', 'true');
if (nextRow) {
// Add event stopping for download buttons in the details row
const downloadButtons = nextRow.querySelectorAll('input[type="image"]');
downloadButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
nextRow.addEventListener('click', clickHandler);
nextRow.setAttribute('data-has-click-handler', 'true');
}
} else {
row.classList.add('warning');
if (row.nextElementSibling) {
row.nextElementSibling.classList.add('warning');
}
}
});
const entries = document.querySelectorAll('.torrent-entry');
entries.forEach(entry => {
// Skip if already has a click handler
if (entry.hasAttribute('data-has-click-handler')) return;
// Add event stopping for buttons in grid view
const deleteButton = entry.querySelector('a[href*="del"]');
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
});
}
const downloadButtons = entry.querySelectorAll('input[type="image"]');
downloadButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
const fileInfoButtons = entry.querySelectorAll('a[rel="facebox"]');
fileInfoButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
entry.addEventListener('click', (e) => {
// Prevent click propagation if this is a delete button
if (e.target.closest('a[href*="del"]') ||
e.target.closest('input[type="image"]') ||
e.target.closest('a[rel="facebox"]')) {
return;
}
// Toggle selection state
entry.classList.toggle('selected');
// Get ID and sync with table view
const id = getIdentifierFromElement(entry);
if (id) {
syncSelectionState(id, entry.classList.contains('selected'));
}
updateFloatingButtonsVisibility();
});
entry.setAttribute('data-has-click-handler', 'true');
});
}
function setupItemHoverEffects() {
const rows = document.querySelectorAll('.tr.g1, .tr.g2');
rows.forEach(row => {
const nextRow = row.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
row.addEventListener('mouseenter', () => {
if (!row.classList.contains('selected')) {
row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
}
});
row.addEventListener('mouseleave', () => {
if (!row.classList.contains('selected')) {
row.style.backgroundColor = '';
nextRow.style.backgroundColor = '';
}
});
nextRow.addEventListener('mouseenter', () => {
if (!row.classList.contains('selected')) {
row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
}
});
nextRow.addEventListener('mouseleave', () => {
if (!row.classList.contains('selected')) {
row.style.backgroundColor = '';
nextRow.style.backgroundColor = '';
}
});
}
});
const entries = document.querySelectorAll('.torrent-entry');
entries.forEach(entry => {
entry.addEventListener('mouseenter', () => {
if (!entry.classList.contains('selected')) {
entry.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
}
});
entry.addEventListener('mouseleave', () => {
if (!entry.classList.contains('selected')) {
entry.style.backgroundColor = '';
}
});
});
}
function getSelectedItemLinks() {
// Use a Set to store unique links and prevent duplication
const uniqueLinks = new Set();
const uniqueIds = new Set();
// Process selected rows in table view
const selectedRows = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected');
selectedRows.forEach(row => {
// Extract torrent ID to prevent duplicates
const id = getIdentifierFromElement(row);
if (id && !uniqueIds.has(id)) {
uniqueIds.add(id);
const textarea = row.nextElementSibling.querySelector('textarea');
if (textarea && textarea.value) {
uniqueLinks.add(textarea.value);
}
}
});
// Only process grid items if grid view is active
const gridContainer = document.getElementById('torrent-grid-container');
if (gridContainer && gridContainer.style.display !== 'none') {
const selectedEntries = document.querySelectorAll('.torrent-entry.selected');
selectedEntries.forEach(entry => {
// Extract torrent ID to prevent duplicates
const id = getIdentifierFromElement(entry);
if (id && !uniqueIds.has(id)) {
uniqueIds.add(id);
const textarea = entry.querySelector('textarea');
if (textarea && textarea.value) {
uniqueLinks.add(textarea.value);
}
}
});
}
return Array.from(uniqueLinks);
}
function copySelectedLinksToClipboard() {
const selectedLinks = getSelectedItemLinks();
if (selectedLinks.length > 0) {
const clipboardText = selectedLinks.join('\n');
GM_setClipboard(clipboardText);
}
}
function sendSelectedLinksToDebrid(e) {
e.preventDefault();
const selectedLinks = getSelectedItemLinks();
if (selectedLinks.length > 0) {
const form = document.createElement('form');
form.method = 'POST';
form.action = './downloader';
const input = document.createElement('textarea');
input.name = 'links';
input.value = selectedLinks.join('\n');
form.appendChild(input);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
}
function extractUrlsFromText(text) {
// Enhanced URL regex that better handles various URL formats
const urlRegex = /(?:(?:https?|ftp):\/\/|www\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$])/ig;
const urls = text.match(urlRegex) || [];
// Filter out duplicates and ensure proper http prefix
return [...new Set(urls)].map(url => {
if (!url.startsWith('http')) {
return 'http://' + url;
}
return url;
});
}
function addExtractUrlsButtonToDownloader() {
const textarea = document.getElementById('links');
if (textarea) {
const button = document.createElement('button');
button.id = 'extractUrlsButton';
button.textContent = 'Extract URLs';
button.addEventListener('click', function(e) {
e.preventDefault();
const content = textarea.value;
const urls = extractUrlsFromText(content);
// Add visual feedback
addButtonClickFeedback(button);
if (urls.length > 0) {
textarea.value = urls.join('\n');
// Visual feedback
button.textContent = `${urls.length} URLs Found`;
setTimeout(() => {
button.textContent = 'Extract URLs';
}, 2000);
} else {
button.textContent = 'No URLs Found';
setTimeout(() => {
button.textContent = 'Extract URLs';
}, 2000);
}
});
textarea.parentNode.style.position = 'relative';
textarea.parentNode.appendChild(button);
}
}
function addCopyLinksButton() {
const linksContainer = document.querySelector('#links-container');
if (linksContainer && linksContainer.children.length > 0) {
const originalButton = document.querySelector('#sub_links');
if (originalButton) {
const copyButton = originalButton.cloneNode(true);
copyButton.id = 'copy_links';
copyButton.value = 'Copy links';
copyButton.type = 'button';
copyButton.style.display = 'block';
copyButton.style.margin = '0 auto';
copyButton.style.float = 'none'
copyButton.style.marginBottom = '10px'
copyButton.addEventListener('click', function(e) {
e.preventDefault();
const links = Array.from(document.querySelectorAll('#links-container .link-generated a'))
.filter(a => a.textContent.includes('DOWNLOAD'))
.map(a => a.href)
.join('\n');
if (links) {
GM_setClipboard(links);
// Add visual feedback (for input elements)
copyButton.classList.add('button-clicked');
const originalValue = copyButton.value;
copyButton.value = 'Copied!';
setTimeout(() => {
copyButton.classList.remove('button-clicked');
copyButton.value = originalValue;
}, 500);
}
});
linksContainer.insertAdjacentElement('afterend', copyButton);
}
}
}
function cleanupTorrentPageLayout() {
const textContainer = document.querySelector('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper');
if (textContainer) {
Array.from(textContainer.childNodes).forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
node.remove();
}
});
}
const brElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper br');
brElements.forEach(br => br.remove());
const centerElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper center');
centerElements.forEach(center => center.remove());
const contentSeparatorMiniElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper div.content_separator_mini');
contentSeparatorMiniElements.forEach(div => div.remove());
const h2Elements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper h2');
h2Elements.forEach(h2 => h2.remove());
const spanElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper span.px10');
spanElements.forEach(span => span.remove());
}
function redesignTorrentInfoWindow() {
const facebox = document.getElementById('facebox');
if (facebox) {
const content = facebox.querySelector('.content');
if (content) {
// Count torrent sections by splitting on <h2> tags
const torrentInfos = content.innerHTML.split('<h2>Torrent Files</h2>').filter(info => info.trim() !== '');
// Only apply grid layout if 3+ torrents
if (torrentInfos.length < 3) return;
// Add class for CSS to apply instead of inline styles
content.classList.add('grid-layout');
// Add class to facebox itself for positioning
facebox.classList.add('grid-layout');
// Store the original buttons with their event listeners
const startButtons = Array.from(content.querySelectorAll('input[type="button"][value="Start my torrent"]'));
content.innerHTML = '';
torrentInfos.forEach((info, index) => {
const div = document.createElement('div');
div.className = 'torrent-info';
// Create a temporary div to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = '<h2>Torrent Files</h2>' + info;
// Move the content except the button
while (tempDiv.firstChild) {
if (tempDiv.firstChild.tagName !== 'INPUT' || tempDiv.firstChild.type !== 'button') {
div.appendChild(tempDiv.firstChild);
} else {
tempDiv.removeChild(tempDiv.firstChild);
}
}
// Append the original button with its event listeners
if (startButtons[index]) {
div.appendChild(startButtons[index]);
}
content.appendChild(div);
});
}
}
}
function setupTorrentInfoWindowObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
for (let node of mutation.addedNodes) {
if (node.id === 'facebox') {
redesignTorrentInfoWindow();
}
}
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
function checkForTorrentInfoWindow() {
const intervalId = setInterval(() => {
const facebox = document.getElementById('facebox');
if (facebox) {
redesignTorrentInfoWindow();
clearInterval(intervalId);
}
}, 1000);
}
function createGridLayout(columnCount) {
const table = document.querySelector('table[width="100%"]');
if (!table) return;
// First, check if grid already exists and remove it
const existingGrid = document.getElementById('torrent-grid-container');
if (existingGrid) {
existingGrid.remove();
}
// Create grid container
const container = document.createElement('div');
container.id = 'torrent-grid-container';
container.style.display = 'flex';
container.style.flexWrap = 'wrap';
container.style.justifyContent = 'space-between';
// Create grid items from table rows
const rows = table.querySelectorAll('tr');
for (let i = 1; i < rows.length; i += 2) {
// Check if original row is selected
const isSelected = rows[i].classList.contains('selected');
const torrentDiv = createGridItemFromTableRows(rows[i], rows[i + 1], isSelected);
container.appendChild(torrentDiv);
}
// Insert grid after the table
table.parentNode.insertBefore(container, table.nextSibling);
// Hide the table but keep it in the DOM
table.style.display = 'none';
// Mark the table for later reference
table.id = 'original-torrent-table';
applyGridLayoutStyles(columnCount);
adjustImageSizeInNewLayout();
moveDeleteLinkToEnd();
// Apply enhanced selection handling
setupGridItemsEventHandlers();
updateFloatingButtonsVisibility(); // Update button visibility to reflect current selections
}
function applyGridLayoutStyles(columnCount) {
const width = `calc(${100 / columnCount}% - 20px)`;
GM_addStyle(`
#torrent-grid-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.torrent-entry {
width: ${width};
margin-bottom: 20px;
border: 1px solid #ccc;
padding: 10px;
box-sizing: border-box;
cursor: pointer;
position: relative;
}
/* Fix for long filenames with dots */
.torrent-entry span[id^="name_"] {
display: block;
word-break: break-all;
overflow-wrap: break-word;
white-space: normal;
width: 100%;
margin-bottom: 5px;
font-weight: bold;
}
.torrent-entry td {
display: block;
width: 100%;
}
.torrent-entry tr {
display: block;
}
.torrent-entry form {
margin-top: 10px;
}
.torrent-entry textarea {
min-height: 2.5em;
max-height: 6em;
overflow-y: auto;
resize: vertical;
}
`);
}
function adjustImageSizeInNewLayout() {
document.querySelectorAll('#torrent-grid-container .torrent-entry form input[type="image"]').forEach(function(img) {
img.style.width = '10%';
img.style.height = 'auto';
img.style.display = 'inline-block';
img.style.marginLeft = '10px';
});
document.querySelectorAll('#torrent-grid-container .torrent-entry form').forEach(function(form) {
form.style.display = 'flex';
form.style.alignItems = 'center';
});
}
function moveDeleteLinkToEnd() {
document.querySelectorAll('.torrent-entry').forEach(entry => {
const deleteLink = entry.querySelector('a[href*="del"]');
if (deleteLink) {
// Create a container for the delete link
const deleteContainer = document.createElement('div');
deleteContainer.classList.add('delete-container');
deleteContainer.style.position = 'absolute';
deleteContainer.style.right = '0';
deleteContainer.style.top = '0';
deleteContainer.style.display = 'flex';
deleteContainer.style.alignItems = 'center';
deleteContainer.style.height = '100%';
deleteContainer.style.paddingRight = '10px';
// Move the delete link into the new container
deleteContainer.appendChild(deleteLink);
entry.appendChild(deleteContainer);
// Ensure the parent .torrent-entry has relative positioning
entry.style.position = 'relative';
}
});
}
function createGridItemFromTableRows(mainRow, detailRow, isSelected = false) {
const div = document.createElement('div');
div.className = 'torrent-entry';
div.innerHTML = mainRow.innerHTML + detailRow.innerHTML;
// Set selected state if the original row was selected
if (isSelected) {
div.classList.add('selected');
div.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
}
return div;
}
// Get a unique identifier from an element (row or grid item)
function getIdentifierFromElement(element) {
// Try to find a unique ID in the element (torrent ID, name ID, etc.)
const idElement = element.querySelector('[id^="name_"], [id^="link_"], [id^="status_"]');
if (idElement) {
return idElement.id;
}
return null;
}
// Sync selection state between table and grid views
function syncSelectionState(id, isSelected) {
if (!id) return;
// Get ID prefix and suffix
const parts = id.split('_');
if (parts.length < 2) return;
const prefix = parts[0];
const suffix = parts[1];
// Get all elements with IDs containing this suffix (both in table and grid)
const selector = `[id$="_${suffix}"]`;
const relatedElements = document.querySelectorAll(selector);
// Find related rows and grid items
let tableRows = [];
let gridItems = [];
relatedElements.forEach(el => {
// Find containing row
let row = el.closest('.tr.g1, .tr.g2');
if (row) {
tableRows.push(row);
// Also get the next row (detail row)
if (row.nextElementSibling && !row.nextElementSibling.classList.contains('g1') &&
!row.nextElementSibling.classList.contains('g2')) {
tableRows.push(row.nextElementSibling);
}
}
// Find containing grid item
let gridItem = el.closest('.torrent-entry');
if (gridItem) {
gridItems.push(gridItem);
}
});
// Apply selection state to all related elements
tableRows = [...new Set(tableRows)]; // Remove duplicates
tableRows.forEach(row => {
if (isSelected) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
gridItems = [...new Set(gridItems)]; // Remove duplicates
gridItems.forEach(item => {
if (isSelected) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
function addSwitchToGridLayoutButton() {
const button = document.createElement('button');
button.textContent = 'Switch to Grid Layout';
button.id = 'switchLayoutButton';
button.style.position = 'fixed';
button.style.top = '10px';
button.style.right = '20px';
button.style.zIndex = '1000';
button.setAttribute('data-current-layout', 'table');
button.addEventListener('click', (e) => {
// Add visual feedback
addButtonClickFeedback(button);
toggleLayout();
});
document.body.appendChild(button);
}
function toggleLayout() {
const button = document.getElementById('switchLayoutButton');
const currentLayout = button.getAttribute('data-current-layout');
if (currentLayout === 'table') {
// Switch to grid layout
const columnCount = 3;
createGridLayout(columnCount);
button.textContent = 'Switch to Table Layout';
button.setAttribute('data-current-layout', 'grid');
} else {
// Switch back to table layout without reload
const gridContainer = document.getElementById('torrent-grid-container');
const originalTable = document.getElementById('original-torrent-table');
if (gridContainer && originalTable) {
// Hide grid, show table
gridContainer.style.display = 'none';
originalTable.style.display = 'table';
button.textContent = 'Switch to Grid Layout';
button.setAttribute('data-current-layout', 'table');
}
}
// Update floating buttons visibility
updateFloatingButtonsVisibility();
}
function switchToGridLayout() {
const button = document.getElementById('switchLayoutButton');
if (button.getAttribute('data-current-layout') === 'table') {
toggleLayout();
}
}
function deleteSelectedTorrents() {
const selectedItems = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected');
const deleteIds = [];
selectedItems.forEach(item => {
// Find delete link within the item
const deleteLink = item.querySelector('a[href*="del="]');
if (deleteLink) {
const href = deleteLink.getAttribute('href');
const match = href.match(/del=([^&]+)/);
if (match && match[1]) {
deleteIds.push(match[1]);
}
}
});
if (deleteIds.length === 0) return;
if (confirm(`Delete ${deleteIds.length} selected torrents?`)) {
// Change button text to "Deleting..." after confirmation
const originalWidth = deleteButton.offsetWidth;
deleteButton.textContent = 'Deleting...';
deleteButton.style.width = `${originalWidth}px`;
// Process deletions sequentially to avoid overwhelming the server
deleteSequentially(deleteIds, 0);
}
}
function deleteSequentially(ids, index) {
if (index >= ids.length) {
// All done, refresh the page
window.location.reload();
return;
}
const id = ids[index];
const xhr = new XMLHttpRequest();
xhr.open('GET', `?p=1&del=${id}`, true);
xhr.onload = function() {
// Move to next deletion
deleteSequentially(ids, index + 1);
};
xhr.onerror = function() {
// Still try the next one
deleteSequentially(ids, index + 1);
};
xhr.send();
}
function setupGridItemsEventHandlers() {
const entries = document.querySelectorAll('.torrent-entry');
entries.forEach(entry => {
// Clear any existing handlers by cloning the node
const newEntry = entry.cloneNode(true);
entry.parentNode.replaceChild(newEntry, entry);
// Add event stopping for buttons in grid view
const deleteButton = newEntry.querySelector('a[href*="del"]');
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
});
}
const downloadButtons = newEntry.querySelectorAll('input[type="image"]');
downloadButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
const fileInfoButtons = newEntry.querySelectorAll('a[rel="facebox"]');
fileInfoButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
// Main click handler for selection toggling
newEntry.addEventListener('click', (e) => {
// Prevent click propagation if this is a button
if (e.target.closest('a[href*="del"]') ||
e.target.closest('input[type="image"]') ||
e.target.closest('a[rel="facebox"]')) {
return;
}
// Toggle selection state
newEntry.classList.toggle('selected');
if (newEntry.classList.contains('selected')) {
newEntry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
} else {
newEntry.style.backgroundColor = '';
}
// Get ID and sync with table view
const id = getIdentifierFromElement(newEntry);
if (id) {
const isNowSelected = newEntry.classList.contains('selected');
syncTableViewSelection(id, isNowSelected);
}
updateFloatingButtonsVisibility();
});
});
}
function syncTableViewSelection(id, isSelected) {
if (!id) return;
// Get ID suffix
const parts = id.split('_');
if (parts.length < 2) return;
const suffix = parts[1];
// Find table rows with this torrent ID
const selector = `[id$="_${suffix}"]`;
const originalTable = document.getElementById('original-torrent-table');
if (!originalTable) return;
const elements = originalTable.querySelectorAll(selector);
elements.forEach(el => {
const row = el.closest('.tr.g1, .tr.g2');
if (row) {
// Set selection state on main row
if (isSelected) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
// Set selection state on detail row
const nextRow = row.nextElementSibling;
if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
if (isSelected) {
nextRow.classList.add('selected');
} else {
nextRow.classList.remove('selected');
}
}
}
});
}
// Helper function to add visual feedback to buttons
function addButtonClickFeedback(button, tempText = null) {
// Store original text if we're changing it
const originalText = tempText ? button.textContent : null;
// Store original width to prevent layout shifts
const originalWidth = button.offsetWidth;
// Add animation class
button.classList.add('button-clicked');
// Change text if specified
if (tempText) {
button.textContent = tempText;
// Ensure width doesn't change
button.style.width = `${originalWidth}px`;
}
// Remove animation class and restore text after animation
setTimeout(() => {
button.classList.remove('button-clicked');
if (originalText) {
button.textContent = originalText;
// Remove explicit width to allow natural sizing again
button.style.width = '';
}
}, 500);
}
if (document.readyState === 'complete') {
initializeApplication();
} else {
window.addEventListener('load', initializeApplication);
}
})();