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