Real-Debrid Enhancer

Enhance Real-Debrid with clickable rows, copy and debrid buttons, grid layout, and improved layout management on torrents and downloader pages.

目前为 2025-03-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Real-Debrid Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0
  5. // @description Enhance Real-Debrid with clickable rows, copy and debrid buttons, grid layout, and improved layout management on torrents and downloader pages.
  6. // @author UnderPL
  7. // @license MIT
  8. // @match https://real-debrid.com/torrents*
  9. // @match https://real-debrid.com/
  10. // @match https://real-debrid.com/downloader*
  11. // @grant GM_setClipboard
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. let copyButton, debridButton, deleteButton;
  19.  
  20. GM_addStyle(`
  21. /* Selection styling */
  22. .tr.g1:not(.warning), .tr.g2:not(.warning), .tr.g1:not(.warning) + tr, .tr.g2:not(.warning) + tr {
  23. cursor: pointer;
  24. position: relative;
  25. transition: all 0.2s ease-in-out;
  26. }
  27. .tr.g1.selected, .tr.g2.selected, .tr.g1.selected + tr, .tr.g2.selected + tr {
  28. background-color: rgba(40, 167, 69, 0.15) !important;
  29. border-left: 4px solid #28a745 !important;
  30. box-shadow: 0 2px 4px rgba(40, 167, 69, 0.1);
  31. }
  32.  
  33. .tr.g1:hover:not(.selected):not(.warning),
  34. .tr.g2:hover:not(.selected):not(.warning),
  35. .tr.g1:hover:not(.selected):not(.warning) + tr,
  36. .tr.g2:hover:not(.selected):not(.warning) + tr {
  37. background-color: rgba(40, 167, 69, 0.05);
  38. }
  39.  
  40. .torrent-entry {
  41. transition: all 0.2s ease-in-out;
  42. border: 1px solid transparent;
  43. }
  44.  
  45. .torrent-entry.selected {
  46. background-color: rgba(40, 167, 69, 0.15) !important;
  47. border: 1px solid #28a745 !important;
  48. box-shadow: 0 2px 4px rgba(40, 167, 69, 0.1);
  49. transform: translateY(-1px);
  50. }
  51.  
  52. .torrent-entry:hover:not(.selected) {
  53. background-color: rgba(40, 167, 69, 0.05);
  54. transform: translateY(-1px);
  55. }
  56.  
  57. .tr.g1, .tr.g2 {
  58. border-top: 2px solid black/* Green border on top */
  59.  
  60. }
  61.  
  62. .tr.g1 + tr, .tr.g2 + tr {
  63. border-bottom: 2px solid black; /* Green border on bottom */
  64.  
  65. }
  66. #buttonContainer {
  67. position: fixed;
  68. bottom: 20px;
  69. right: 20px;
  70. display: flex;
  71. flex-direction: column;
  72. gap: 12px;
  73. z-index: 9999;
  74. }
  75. #buttonContainer button {
  76. padding: 12px 20px;
  77. background-color: #4CAF50;
  78. color: white;
  79. border: none;
  80. border-radius: 10px;
  81. cursor: pointer;
  82. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  83. font-size: 14px;
  84. font-weight: 500;
  85. letter-spacing: 0.3px;
  86. transition: all 0.2s ease;
  87. box-shadow: 0 3px 6px rgba(0,0,0,0.16);
  88. min-width: 200px;
  89. width: 250px; /* Fixed width for all buttons */
  90. text-align: center;
  91. text-transform: uppercase;
  92. white-space: nowrap; /* Prevent text wrapping */
  93. overflow: hidden; /* Hide overflow text */
  94. text-overflow: ellipsis; /* Show ellipsis for overflow */
  95. }
  96. #buttonContainer button:hover {
  97. transform: translateY(-2px);
  98. box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  99. filter: brightness(1.05);
  100. }
  101. #buttonContainer button:active {
  102. transform: translateY(1px);
  103. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  104. }
  105. /* Button click animation */
  106. .button-clicked {
  107. animation: button-click-animation 0.5s ease;
  108. background-color: #3a8a3e !important; /* Darker shade */
  109. }
  110. @keyframes button-click-animation {
  111. 0% { transform: scale(1); }
  112. 50% { transform: scale(0.95); }
  113. 100% { transform: scale(1); }
  114. }
  115. /* Only apply grid layout when the class is present */
  116. #facebox .content.grid-layout {
  117. width: 90vw !important;
  118. max-width: 1200px !important;
  119. display: flex !important;
  120. flex-wrap: wrap !important;
  121. justify-content: space-between !important;
  122. }
  123. /* Center the facebox when grid layout is applied */
  124. #facebox.grid-layout {
  125. left: 50% !important;
  126. transform: translateX(-50%) !important;
  127. }
  128. .torrent-info {
  129. width: calc(33.33% - 20px);
  130. margin-bottom: 20px;
  131. border: 1px solid #ccc;
  132. padding: 10px;
  133. box-sizing: border-box;
  134. }
  135. #switchLayoutButton {
  136. padding: 12px 20px !important;
  137. background-color: #2196F3 !important;
  138. color: white !important;
  139. border: none !important;
  140. border-radius: 10px !important;
  141. cursor: pointer !important;
  142. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
  143. font-size: 14px !important;
  144. font-weight: 500 !important;
  145. letter-spacing: 0.3px !important;
  146. transition: all 0.2s ease !important;
  147. box-shadow: 0 3px 6px rgba(0,0,0,0.16) !important;
  148. text-transform: uppercase !important;
  149. }
  150. #switchLayoutButton:hover {
  151. transform: translateY(-2px) !important;
  152. box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
  153. filter: brightness(1.05) !important;
  154. }
  155. #switchLayoutButton:active {
  156. transform: translateY(1px) !important;
  157. box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
  158. }
  159. #extractUrlsButton {
  160. padding: 8px 12px;
  161. background-color: #2196F3;
  162. color: white;
  163. border: none;
  164. border-radius: 6px;
  165. cursor: pointer;
  166. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  167. font-size: 13px;
  168. font-weight: 500;
  169. letter-spacing: 0.3px;
  170. transition: all 0.2s ease;
  171. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  172. text-transform: uppercase;
  173. position: absolute;
  174. right: 10px;
  175. top: 10px;
  176. }
  177. #extractUrlsButton:hover {
  178. transform: translateY(-1px);
  179. box-shadow: 0 3px 6px rgba(0,0,0,0.15);
  180. filter: brightness(1.05);
  181. }
  182. #extractUrlsButton:active {
  183. transform: translateY(1px);
  184. box-shadow: 0 1px 2px rgba(0,0,0,0.1);
  185. }
  186. `);
  187.  
  188. function initializeApplication() {
  189. if (window.location.href.includes('/torrents')) {
  190. cleanupTorrentPageLayout();
  191. createFloatingButtons();
  192. makeItemsSelectable();
  193. updateFloatingButtonsVisibility();
  194. setupTorrentInfoWindowObserver();
  195. checkForTorrentInfoWindow();
  196. setupItemHoverEffects();
  197. movePaginationToBottomRight();
  198. addSwitchToGridLayoutButton(); // Comment this and uncomment line below to automatically switch to the more compact version of the torrent page
  199. //switchToGridLayout()
  200. }
  201.  
  202. if (window.location.href === 'https://real-debrid.com/' || window.location.href.includes('/downloader')) {
  203. addExtractUrlsButtonToDownloader();
  204. addCopyLinksButton();
  205. }
  206. }
  207.  
  208. function movePaginationToBottomRight() {
  209. const parentElement = document.querySelector('div.full_width_wrapper');
  210. const formElement = parentElement.querySelector('form:nth-child(1)');
  211. const pageElements = parentElement.querySelectorAll('div.full_width_wrapper > strong, div.full_width_wrapper > a[href^="./torrents?p="]');
  212. const containerDiv = document.createElement('div');
  213. const marginSize = '5px';
  214. const fontSize = '16px';
  215.  
  216. containerDiv.style.position = 'absolute';
  217. containerDiv.style.right = '0';
  218. containerDiv.style.bottom = '0';
  219. containerDiv.style.display = 'flex';
  220. containerDiv.style.gap = marginSize;
  221. containerDiv.style.fontSize = fontSize;
  222.  
  223. pageElements.forEach(page => {
  224. containerDiv.appendChild(page);
  225. });
  226.  
  227. formElement.style.position = 'relative';
  228. formElement.appendChild(containerDiv);
  229. // Add selection buttons
  230. addSelectionButtons(formElement);
  231. }
  232. function addSelectionButtons(formElement) {
  233. // Create button container
  234. const buttonContainer = document.createElement('div');
  235. buttonContainer.id = 'selectionButtonsContainer';
  236. buttonContainer.style.display = 'inline-block';
  237. buttonContainer.style.marginLeft = '10px';
  238. buttonContainer.style.gap = '10px';
  239. // Create Select All button
  240. const selectAllButton = document.createElement('button');
  241. selectAllButton.id = 'selectAllButton';
  242. selectAllButton.textContent = 'Select All';
  243. selectAllButton.type = 'button'; // Prevent form submission
  244. selectAllButton.className = 'selection-control-button';
  245. selectAllButton.addEventListener('click', (e) => {
  246. // Add visual feedback without text change
  247. addButtonClickFeedback(selectAllButton);
  248. selectAllItems();
  249. });
  250. // Create Unselect All button
  251. const unselectAllButton = document.createElement('button');
  252. unselectAllButton.id = 'unselectAllButton';
  253. unselectAllButton.textContent = 'Unselect All';
  254. unselectAllButton.type = 'button'; // Prevent form submission
  255. unselectAllButton.className = 'selection-control-button';
  256. unselectAllButton.addEventListener('click', (e) => {
  257. // Add visual feedback without text change
  258. addButtonClickFeedback(unselectAllButton);
  259. unselectAllItems();
  260. });
  261. // Create Reverse Selection button (hidden initially using opacity instead of display:none)
  262. const reverseSelectionButton = document.createElement('button');
  263. reverseSelectionButton.id = 'reverseSelectionButton';
  264. reverseSelectionButton.textContent = 'Invert Selection';
  265. reverseSelectionButton.type = 'button'; // Prevent form submission
  266. reverseSelectionButton.className = 'selection-control-button';
  267. // Use opacity and pointer-events to hide rather than display:none
  268. reverseSelectionButton.style.opacity = '0';
  269. reverseSelectionButton.style.pointerEvents = 'none';
  270. reverseSelectionButton.style.transition = 'opacity 0.2s ease';
  271. reverseSelectionButton.addEventListener('click', (e) => {
  272. // Add visual feedback without text change
  273. addButtonClickFeedback(reverseSelectionButton);
  274. reverseSelection();
  275. });
  276. // Add buttons to container
  277. buttonContainer.appendChild(selectAllButton);
  278. buttonContainer.appendChild(unselectAllButton);
  279. buttonContainer.appendChild(reverseSelectionButton);
  280. // Find the Convert button and insert our buttons after it
  281. const convertButton = formElement.querySelector('input[value="Convert"]');
  282. if (convertButton) {
  283. // Insert after the Convert button
  284. convertButton.insertAdjacentElement('afterend', buttonContainer);
  285. } else {
  286. // Fallback - just append to the form
  287. formElement.appendChild(buttonContainer);
  288. }
  289. // Add CSS for buttons
  290. GM_addStyle(`
  291. .selection-control-button {
  292. padding: 8px 12px;
  293. background-color: #2196F3;
  294. color: white;
  295. border: none;
  296. border-radius: 6px;
  297. cursor: pointer;
  298. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  299. font-size: 13px;
  300. font-weight: 500;
  301. letter-spacing: 0.3px;
  302. transition: all 0.2s ease;
  303. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  304. text-transform: uppercase;
  305. margin-right: 5px;
  306. display: inline-block;
  307. min-width: 120px; /* Minimum width for selection buttons */
  308. text-align: center;
  309. white-space: nowrap; /* Prevent text wrapping */
  310. }
  311. .selection-control-button:hover {
  312. transform: translateY(-1px);
  313. box-shadow: 0 3px 6px rgba(0,0,0,0.15);
  314. filter: brightness(1.05);
  315. }
  316. .selection-control-button:active {
  317. transform: translateY(1px);
  318. box-shadow: 0 1px 2px rgba(0,0,0,0.1);
  319. }
  320. .selection-control-button.button-clicked {
  321. background-color: #1976D2 !important; /* Darker blue */
  322. }
  323. #selectionButtonsContainer {
  324. vertical-align: middle;
  325. }
  326. `);
  327. }
  328. function selectAllItems() {
  329. // Get all selectable items in current view
  330. const gridContainer = document.getElementById('torrent-grid-container');
  331. const isGridActive = gridContainer && gridContainer.style.display !== 'none';
  332. if (isGridActive) {
  333. // Select all grid items
  334. const entries = document.querySelectorAll('.torrent-entry:not(.warning)');
  335. entries.forEach(entry => {
  336. if (!entry.classList.contains('selected')) {
  337. entry.classList.add('selected');
  338. entry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
  339. // Get ID and sync with table view
  340. const id = getIdentifierFromElement(entry);
  341. if (id) {
  342. syncTableViewSelection(id, true);
  343. }
  344. }
  345. });
  346. } else {
  347. // Select all table rows
  348. const rows = document.querySelectorAll('.tr.g1:not(.warning), .tr.g2:not(.warning)');
  349. rows.forEach(row => {
  350. if (!row.classList.contains('selected')) {
  351. row.classList.add('selected');
  352. const nextRow = row.nextElementSibling;
  353. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  354. nextRow.classList.add('selected');
  355. }
  356. // Get ID and sync with grid view
  357. const id = getIdentifierFromElement(row);
  358. if (id) {
  359. syncSelectionState(id, true);
  360. }
  361. }
  362. });
  363. }
  364. updateFloatingButtonsVisibility();
  365. updateReverseSelectionButtonVisibility();
  366. }
  367. function unselectAllItems() {
  368. // Unselect all items in both views
  369. document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected').forEach(item => {
  370. item.classList.remove('selected');
  371. item.style.backgroundColor = '';
  372. // For table rows, also unselect detail row
  373. if (item.classList.contains('g1') || item.classList.contains('g2')) {
  374. const nextRow = item.nextElementSibling;
  375. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  376. nextRow.classList.remove('selected');
  377. nextRow.style.backgroundColor = '';
  378. }
  379. }
  380. });
  381. updateFloatingButtonsVisibility();
  382. updateReverseSelectionButtonVisibility();
  383. }
  384. function reverseSelection() {
  385. // Get all selectable items in current view
  386. const gridContainer = document.getElementById('torrent-grid-container');
  387. const isGridActive = gridContainer && gridContainer.style.display !== 'none';
  388. if (isGridActive) {
  389. // Reverse selection in grid view
  390. const entries = document.querySelectorAll('.torrent-entry:not(.warning)');
  391. entries.forEach(entry => {
  392. const isSelected = entry.classList.contains('selected');
  393. if (isSelected) {
  394. // Properly remove selection styles
  395. entry.classList.remove('selected');
  396. entry.style.backgroundColor = '';
  397. } else {
  398. entry.classList.add('selected');
  399. entry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
  400. }
  401. // Get ID and sync with table view
  402. const id = getIdentifierFromElement(entry);
  403. if (id) {
  404. syncTableViewSelection(id, !isSelected);
  405. }
  406. });
  407. } else {
  408. // Reverse selection in table view
  409. const rows = document.querySelectorAll('.tr.g1:not(.warning), .tr.g2:not(.warning)');
  410. rows.forEach(row => {
  411. const isSelected = row.classList.contains('selected');
  412. if (isSelected) {
  413. // Properly remove selection styles
  414. row.classList.remove('selected');
  415. row.style.backgroundColor = '';
  416. const nextRow = row.nextElementSibling;
  417. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  418. nextRow.classList.remove('selected');
  419. nextRow.style.backgroundColor = '';
  420. }
  421. } else {
  422. row.classList.add('selected');
  423. const nextRow = row.nextElementSibling;
  424. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  425. nextRow.classList.add('selected');
  426. }
  427. }
  428. // Get ID and sync with grid view
  429. const id = getIdentifierFromElement(row);
  430. if (id) {
  431. syncSelectionState(id, !isSelected);
  432. }
  433. });
  434. }
  435. updateFloatingButtonsVisibility();
  436. updateReverseSelectionButtonVisibility();
  437. }
  438. function updateReverseSelectionButtonVisibility() {
  439. const reverseButton = document.getElementById('reverseSelectionButton');
  440. if (!reverseButton) return;
  441. const hasSelectedItems = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected').length > 0;
  442. // Use opacity instead of display to show/hide
  443. if (hasSelectedItems) {
  444. reverseButton.style.opacity = '1';
  445. reverseButton.style.pointerEvents = 'auto';
  446. } else {
  447. reverseButton.style.opacity = '0';
  448. reverseButton.style.pointerEvents = 'none';
  449. }
  450. }
  451.  
  452. function createFloatingButtons() {
  453. const container = document.createElement('div');
  454. container.id = 'buttonContainer';
  455.  
  456. debridButton = document.createElement('button');
  457. debridButton.addEventListener('click', (e) => {
  458. // Add visual feedback
  459. addButtonClickFeedback(debridButton, 'Sent to Debrid');
  460. sendSelectedLinksToDebrid(e);
  461. });
  462.  
  463. copyButton = document.createElement('button');
  464. copyButton.addEventListener('click', (e) => {
  465. // Add visual feedback
  466. addButtonClickFeedback(copyButton, 'Copied!');
  467. copySelectedLinksToClipboard();
  468. });
  469. // Add delete button
  470. deleteButton = document.createElement('button');
  471. deleteButton.style.backgroundColor = '#dc3545';
  472. deleteButton.addEventListener('click', (e) => {
  473. addButtonClickFeedback(deleteButton);
  474. deleteSelectedTorrents();
  475. });
  476.  
  477. container.appendChild(debridButton);
  478. container.appendChild(copyButton);
  479. container.appendChild(deleteButton);
  480. document.body.appendChild(container);
  481.  
  482. return container;
  483. }
  484.  
  485. function updateFloatingButtonsVisibility() {
  486. const selectedLinks = getSelectedItemLinks();
  487. const count = selectedLinks.length;
  488. // Get unique selected items count
  489. const uniqueSelectedIds = getUniqueSelectedItemsCount();
  490. const itemCount = uniqueSelectedIds.length;
  491.  
  492. if (count > 0) {
  493. debridButton.textContent = `Debrid (${count})`;
  494. copyButton.textContent = `Copy Selected (${count})`;
  495. deleteButton.textContent = `Delete (${itemCount})`;
  496. debridButton.style.display = 'block';
  497. copyButton.style.display = 'block';
  498. deleteButton.style.display = 'block';
  499. } else {
  500. debridButton.style.display = 'none';
  501. copyButton.style.display = 'none';
  502. deleteButton.style.display = 'none';
  503. }
  504. // Update visibility of Reverse Selection button
  505. updateReverseSelectionButtonVisibility();
  506. }
  507.  
  508. function getUniqueSelectedItemsCount() {
  509. const uniqueIds = new Set();
  510. const gridContainer = document.getElementById('torrent-grid-container');
  511. const isGridActive = gridContainer && gridContainer.style.display !== 'none';
  512. if (isGridActive) {
  513. // Count only grid items if grid view is active
  514. const selectedEntries = document.querySelectorAll('.torrent-entry.selected');
  515. selectedEntries.forEach(entry => {
  516. const id = getIdentifierFromElement(entry);
  517. if (id) uniqueIds.add(id);
  518. });
  519. } else {
  520. // Count only table rows if table view is active
  521. const selectedRows = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected');
  522. selectedRows.forEach(row => {
  523. const id = getIdentifierFromElement(row);
  524. if (id) uniqueIds.add(id);
  525. });
  526. }
  527. return Array.from(uniqueIds);
  528. }
  529.  
  530. function makeItemsSelectable() {
  531. const rows = document.querySelectorAll('.tr.g1, .tr.g2');
  532. rows.forEach(row => {
  533. // Skip if already has a click handler
  534. if (row.hasAttribute('data-has-click-handler')) return;
  535. const warningSpan = row.querySelector('span.px10 strong');
  536. if (!warningSpan || warningSpan.textContent !== 'Warning:') {
  537. const nextRow = row.nextElementSibling;
  538. // Add event stopping for delete buttons and download images
  539. const deleteButton = row.querySelector('a[href*="del"]');
  540. if (deleteButton) {
  541. deleteButton.addEventListener('click', (e) => {
  542. e.stopPropagation();
  543. });
  544. }
  545. // Add event stopping for file info buttons
  546. const fileInfoButton = row.querySelector('a[rel="facebox"]');
  547. if (fileInfoButton) {
  548. fileInfoButton.addEventListener('click', (e) => {
  549. e.stopPropagation();
  550. });
  551. }
  552. const clickHandler = () => {
  553. row.classList.toggle('selected');
  554. if (nextRow) {
  555. nextRow.classList.toggle('selected');
  556. }
  557. // Get ID and sync with grid view
  558. const id = getIdentifierFromElement(row);
  559. if (id) {
  560. syncSelectionState(id, row.classList.contains('selected'));
  561. }
  562. updateFloatingButtonsVisibility();
  563. };
  564. row.addEventListener('click', clickHandler);
  565. row.setAttribute('data-has-click-handler', 'true');
  566. if (nextRow) {
  567. // Add event stopping for download buttons in the details row
  568. const downloadButtons = nextRow.querySelectorAll('input[type="image"]');
  569. downloadButtons.forEach(button => {
  570. button.addEventListener('click', (e) => {
  571. e.stopPropagation();
  572. });
  573. });
  574. nextRow.addEventListener('click', clickHandler);
  575. nextRow.setAttribute('data-has-click-handler', 'true');
  576. }
  577. } else {
  578. row.classList.add('warning');
  579. if (row.nextElementSibling) {
  580. row.nextElementSibling.classList.add('warning');
  581. }
  582. }
  583. });
  584.  
  585. const entries = document.querySelectorAll('.torrent-entry');
  586. entries.forEach(entry => {
  587. // Skip if already has a click handler
  588. if (entry.hasAttribute('data-has-click-handler')) return;
  589. // Add event stopping for buttons in grid view
  590. const deleteButton = entry.querySelector('a[href*="del"]');
  591. if (deleteButton) {
  592. deleteButton.addEventListener('click', (e) => {
  593. e.stopPropagation();
  594. });
  595. }
  596. const downloadButtons = entry.querySelectorAll('input[type="image"]');
  597. downloadButtons.forEach(button => {
  598. button.addEventListener('click', (e) => {
  599. e.stopPropagation();
  600. });
  601. });
  602. const fileInfoButtons = entry.querySelectorAll('a[rel="facebox"]');
  603. fileInfoButtons.forEach(button => {
  604. button.addEventListener('click', (e) => {
  605. e.stopPropagation();
  606. });
  607. });
  608. entry.addEventListener('click', (e) => {
  609. // Prevent click propagation if this is a delete button
  610. if (e.target.closest('a[href*="del"]') ||
  611. e.target.closest('input[type="image"]') ||
  612. e.target.closest('a[rel="facebox"]')) {
  613. return;
  614. }
  615. // Toggle selection state
  616. entry.classList.toggle('selected');
  617. // Get ID and sync with table view
  618. const id = getIdentifierFromElement(entry);
  619. if (id) {
  620. syncSelectionState(id, entry.classList.contains('selected'));
  621. }
  622. updateFloatingButtonsVisibility();
  623. });
  624. entry.setAttribute('data-has-click-handler', 'true');
  625. });
  626. }
  627.  
  628. function setupItemHoverEffects() {
  629. const rows = document.querySelectorAll('.tr.g1, .tr.g2');
  630. rows.forEach(row => {
  631. const nextRow = row.nextElementSibling;
  632. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  633. row.addEventListener('mouseenter', () => {
  634. if (!row.classList.contains('selected')) {
  635. row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  636. nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  637. }
  638. });
  639. row.addEventListener('mouseleave', () => {
  640. if (!row.classList.contains('selected')) {
  641. row.style.backgroundColor = '';
  642. nextRow.style.backgroundColor = '';
  643. }
  644. });
  645. nextRow.addEventListener('mouseenter', () => {
  646. if (!row.classList.contains('selected')) {
  647. row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  648. nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  649. }
  650. });
  651. nextRow.addEventListener('mouseleave', () => {
  652. if (!row.classList.contains('selected')) {
  653. row.style.backgroundColor = '';
  654. nextRow.style.backgroundColor = '';
  655. }
  656. });
  657. }
  658. });
  659.  
  660. const entries = document.querySelectorAll('.torrent-entry');
  661. entries.forEach(entry => {
  662. entry.addEventListener('mouseenter', () => {
  663. if (!entry.classList.contains('selected')) {
  664. entry.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  665. }
  666. });
  667. entry.addEventListener('mouseleave', () => {
  668. if (!entry.classList.contains('selected')) {
  669. entry.style.backgroundColor = '';
  670. }
  671. });
  672. });
  673. }
  674.  
  675. function getSelectedItemLinks() {
  676. // Use a Set to store unique links and prevent duplication
  677. const uniqueLinks = new Set();
  678. const uniqueIds = new Set();
  679. // Process selected rows in table view
  680. const selectedRows = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected');
  681. selectedRows.forEach(row => {
  682. // Extract torrent ID to prevent duplicates
  683. const id = getIdentifierFromElement(row);
  684. if (id && !uniqueIds.has(id)) {
  685. uniqueIds.add(id);
  686. const textarea = row.nextElementSibling.querySelector('textarea');
  687. if (textarea && textarea.value) {
  688. uniqueLinks.add(textarea.value);
  689. }
  690. }
  691. });
  692. // Only process grid items if grid view is active
  693. const gridContainer = document.getElementById('torrent-grid-container');
  694. if (gridContainer && gridContainer.style.display !== 'none') {
  695. const selectedEntries = document.querySelectorAll('.torrent-entry.selected');
  696. selectedEntries.forEach(entry => {
  697. // Extract torrent ID to prevent duplicates
  698. const id = getIdentifierFromElement(entry);
  699. if (id && !uniqueIds.has(id)) {
  700. uniqueIds.add(id);
  701. const textarea = entry.querySelector('textarea');
  702. if (textarea && textarea.value) {
  703. uniqueLinks.add(textarea.value);
  704. }
  705. }
  706. });
  707. }
  708. return Array.from(uniqueLinks);
  709. }
  710.  
  711. function copySelectedLinksToClipboard() {
  712. const selectedLinks = getSelectedItemLinks();
  713. if (selectedLinks.length > 0) {
  714. const clipboardText = selectedLinks.join('\n');
  715. GM_setClipboard(clipboardText);
  716. }
  717. }
  718.  
  719. function sendSelectedLinksToDebrid(e) {
  720. e.preventDefault();
  721. const selectedLinks = getSelectedItemLinks();
  722. if (selectedLinks.length > 0) {
  723. const form = document.createElement('form');
  724. form.method = 'POST';
  725. form.action = './downloader';
  726.  
  727. const input = document.createElement('textarea');
  728. input.name = 'links';
  729. input.value = selectedLinks.join('\n');
  730. form.appendChild(input);
  731.  
  732. document.body.appendChild(form);
  733. form.submit();
  734. document.body.removeChild(form);
  735. }
  736. }
  737.  
  738. function extractUrlsFromText(text) {
  739. // Enhanced URL regex that better handles various URL formats
  740. const urlRegex = /(?:(?:https?|ftp):\/\/|www\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$])/ig;
  741. const urls = text.match(urlRegex) || [];
  742. // Filter out duplicates and ensure proper http prefix
  743. return [...new Set(urls)].map(url => {
  744. if (!url.startsWith('http')) {
  745. return 'http://' + url;
  746. }
  747. return url;
  748. });
  749. }
  750.  
  751. function addExtractUrlsButtonToDownloader() {
  752. const textarea = document.getElementById('links');
  753. if (textarea) {
  754. const button = document.createElement('button');
  755. button.id = 'extractUrlsButton';
  756. button.textContent = 'Extract URLs';
  757. button.addEventListener('click', function(e) {
  758. e.preventDefault();
  759. const content = textarea.value;
  760. const urls = extractUrlsFromText(content);
  761. // Add visual feedback
  762. addButtonClickFeedback(button);
  763. if (urls.length > 0) {
  764. textarea.value = urls.join('\n');
  765. // Visual feedback
  766. button.textContent = `${urls.length} URLs Found`;
  767. setTimeout(() => {
  768. button.textContent = 'Extract URLs';
  769. }, 2000);
  770. } else {
  771. button.textContent = 'No URLs Found';
  772. setTimeout(() => {
  773. button.textContent = 'Extract URLs';
  774. }, 2000);
  775. }
  776. });
  777.  
  778. textarea.parentNode.style.position = 'relative';
  779. textarea.parentNode.appendChild(button);
  780. }
  781. }
  782.  
  783. function addCopyLinksButton() {
  784. const linksContainer = document.querySelector('#links-container');
  785. if (linksContainer && linksContainer.children.length > 0) {
  786. const originalButton = document.querySelector('#sub_links');
  787. if (originalButton) {
  788. const copyButton = originalButton.cloneNode(true);
  789. copyButton.id = 'copy_links';
  790. copyButton.value = 'Copy links';
  791. copyButton.type = 'button';
  792. copyButton.style.display = 'block';
  793. copyButton.style.margin = '0 auto';
  794. copyButton.style.float = 'none'
  795. copyButton.style.marginBottom = '10px'
  796.  
  797. copyButton.addEventListener('click', function(e) {
  798. e.preventDefault();
  799. const links = Array.from(document.querySelectorAll('#links-container .link-generated a'))
  800. .filter(a => a.textContent.includes('DOWNLOAD'))
  801. .map(a => a.href)
  802. .join('\n');
  803.  
  804. if (links) {
  805. GM_setClipboard(links);
  806. // Add visual feedback (for input elements)
  807. copyButton.classList.add('button-clicked');
  808. const originalValue = copyButton.value;
  809. copyButton.value = 'Copied!';
  810. setTimeout(() => {
  811. copyButton.classList.remove('button-clicked');
  812. copyButton.value = originalValue;
  813. }, 500);
  814. }
  815. });
  816.  
  817. linksContainer.insertAdjacentElement('afterend', copyButton);
  818. }
  819. }
  820. }
  821.  
  822.  
  823. function cleanupTorrentPageLayout() {
  824. 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');
  825. if (textContainer) {
  826. Array.from(textContainer.childNodes).forEach(node => {
  827. if (node.nodeType === Node.TEXT_NODE) {
  828. node.remove();
  829. }
  830. });
  831. }
  832.  
  833. 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');
  834. brElements.forEach(br => br.remove());
  835.  
  836. 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');
  837. centerElements.forEach(center => center.remove());
  838.  
  839. 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');
  840. contentSeparatorMiniElements.forEach(div => div.remove());
  841.  
  842. 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');
  843. h2Elements.forEach(h2 => h2.remove());
  844.  
  845. 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');
  846. spanElements.forEach(span => span.remove());
  847. }
  848.  
  849. function redesignTorrentInfoWindow() {
  850. const facebox = document.getElementById('facebox');
  851. if (facebox) {
  852. const content = facebox.querySelector('.content');
  853. if (content) {
  854. // Count torrent sections by splitting on <h2> tags
  855. const torrentInfos = content.innerHTML.split('<h2>Torrent Files</h2>').filter(info => info.trim() !== '');
  856. // Only apply grid layout if 3+ torrents
  857. if (torrentInfos.length < 3) return;
  858. // Add class for CSS to apply instead of inline styles
  859. content.classList.add('grid-layout');
  860. // Add class to facebox itself for positioning
  861. facebox.classList.add('grid-layout');
  862.  
  863. // Store the original buttons with their event listeners
  864. const startButtons = Array.from(content.querySelectorAll('input[type="button"][value="Start my torrent"]'));
  865.  
  866. content.innerHTML = '';
  867.  
  868. torrentInfos.forEach((info, index) => {
  869. const div = document.createElement('div');
  870. div.className = 'torrent-info';
  871.  
  872. // Create a temporary div to parse the HTML
  873. const tempDiv = document.createElement('div');
  874. tempDiv.innerHTML = '<h2>Torrent Files</h2>' + info;
  875.  
  876. // Move the content except the button
  877. while (tempDiv.firstChild) {
  878. if (tempDiv.firstChild.tagName !== 'INPUT' || tempDiv.firstChild.type !== 'button') {
  879. div.appendChild(tempDiv.firstChild);
  880. } else {
  881. tempDiv.removeChild(tempDiv.firstChild);
  882. }
  883. }
  884.  
  885. // Append the original button with its event listeners
  886. if (startButtons[index]) {
  887. div.appendChild(startButtons[index]);
  888. }
  889.  
  890. content.appendChild(div);
  891. });
  892. }
  893. }
  894. }
  895.  
  896. function setupTorrentInfoWindowObserver() {
  897. const observer = new MutationObserver((mutations) => {
  898. mutations.forEach((mutation) => {
  899. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  900. for (let node of mutation.addedNodes) {
  901. if (node.id === 'facebox') {
  902. redesignTorrentInfoWindow();
  903. }
  904. }
  905. }
  906. });
  907. });
  908.  
  909. observer.observe(document.body, { childList: true, subtree: true });
  910. }
  911.  
  912. function checkForTorrentInfoWindow() {
  913. const intervalId = setInterval(() => {
  914. const facebox = document.getElementById('facebox');
  915. if (facebox) {
  916. redesignTorrentInfoWindow();
  917. clearInterval(intervalId);
  918. }
  919. }, 1000);
  920. }
  921.  
  922. function createGridLayout(columnCount) {
  923. const table = document.querySelector('table[width="100%"]');
  924. if (!table) return;
  925.  
  926. // First, check if grid already exists and remove it
  927. const existingGrid = document.getElementById('torrent-grid-container');
  928. if (existingGrid) {
  929. existingGrid.remove();
  930. }
  931.  
  932. // Create grid container
  933. const container = document.createElement('div');
  934. container.id = 'torrent-grid-container';
  935. container.style.display = 'flex';
  936. container.style.flexWrap = 'wrap';
  937. container.style.justifyContent = 'space-between';
  938.  
  939. // Create grid items from table rows
  940. const rows = table.querySelectorAll('tr');
  941. for (let i = 1; i < rows.length; i += 2) {
  942. // Check if original row is selected
  943. const isSelected = rows[i].classList.contains('selected');
  944. const torrentDiv = createGridItemFromTableRows(rows[i], rows[i + 1], isSelected);
  945. container.appendChild(torrentDiv);
  946. }
  947.  
  948. // Insert grid after the table
  949. table.parentNode.insertBefore(container, table.nextSibling);
  950. // Hide the table but keep it in the DOM
  951. table.style.display = 'none';
  952. // Mark the table for later reference
  953. table.id = 'original-torrent-table';
  954. applyGridLayoutStyles(columnCount);
  955. adjustImageSizeInNewLayout();
  956. moveDeleteLinkToEnd();
  957. // Apply enhanced selection handling
  958. setupGridItemsEventHandlers();
  959. updateFloatingButtonsVisibility(); // Update button visibility to reflect current selections
  960. }
  961.  
  962. function applyGridLayoutStyles(columnCount) {
  963. const width = `calc(${100 / columnCount}% - 20px)`;
  964. GM_addStyle(`
  965. #torrent-grid-container {
  966. width: 100%;
  967. max-width: 1200px;
  968. margin: 0 auto;
  969. }
  970. .torrent-entry {
  971. width: ${width};
  972. margin-bottom: 20px;
  973. border: 1px solid #ccc;
  974. padding: 10px;
  975. box-sizing: border-box;
  976. cursor: pointer;
  977. position: relative;
  978. }
  979. /* Fix for long filenames with dots */
  980. .torrent-entry span[id^="name_"] {
  981. display: block;
  982. word-break: break-all;
  983. overflow-wrap: break-word;
  984. white-space: normal;
  985. width: 100%;
  986. margin-bottom: 5px;
  987. font-weight: bold;
  988. }
  989. .torrent-entry td {
  990. display: block;
  991. width: 100%;
  992. }
  993. .torrent-entry tr {
  994. display: block;
  995. }
  996. .torrent-entry form {
  997. margin-top: 10px;
  998. }
  999. .torrent-entry textarea {
  1000. min-height: 2.5em;
  1001. max-height: 6em;
  1002. overflow-y: auto;
  1003. resize: vertical;
  1004. }
  1005. `);
  1006. }
  1007.  
  1008. function adjustImageSizeInNewLayout() {
  1009. document.querySelectorAll('#torrent-grid-container .torrent-entry form input[type="image"]').forEach(function(img) {
  1010. img.style.width = '10%';
  1011. img.style.height = 'auto';
  1012. img.style.display = 'inline-block';
  1013. img.style.marginLeft = '10px';
  1014. });
  1015.  
  1016. document.querySelectorAll('#torrent-grid-container .torrent-entry form').forEach(function(form) {
  1017. form.style.display = 'flex';
  1018. form.style.alignItems = 'center';
  1019. });
  1020. }
  1021.  
  1022. function moveDeleteLinkToEnd() {
  1023. document.querySelectorAll('.torrent-entry').forEach(entry => {
  1024. const deleteLink = entry.querySelector('a[href*="del"]');
  1025. if (deleteLink) {
  1026. // Create a container for the delete link
  1027. const deleteContainer = document.createElement('div');
  1028. deleteContainer.classList.add('delete-container');
  1029. deleteContainer.style.position = 'absolute';
  1030. deleteContainer.style.right = '0';
  1031. deleteContainer.style.top = '0';
  1032. deleteContainer.style.display = 'flex';
  1033. deleteContainer.style.alignItems = 'center';
  1034. deleteContainer.style.height = '100%';
  1035. deleteContainer.style.paddingRight = '10px';
  1036.  
  1037. // Move the delete link into the new container
  1038. deleteContainer.appendChild(deleteLink);
  1039. entry.appendChild(deleteContainer);
  1040.  
  1041. // Ensure the parent .torrent-entry has relative positioning
  1042. entry.style.position = 'relative';
  1043. }
  1044. });
  1045. }
  1046.  
  1047. function createGridItemFromTableRows(mainRow, detailRow, isSelected = false) {
  1048. const div = document.createElement('div');
  1049. div.className = 'torrent-entry';
  1050. div.innerHTML = mainRow.innerHTML + detailRow.innerHTML;
  1051.  
  1052. // Set selected state if the original row was selected
  1053. if (isSelected) {
  1054. div.classList.add('selected');
  1055. div.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
  1056. }
  1057.  
  1058. return div;
  1059. }
  1060. // Get a unique identifier from an element (row or grid item)
  1061. function getIdentifierFromElement(element) {
  1062. // Try to find a unique ID in the element (torrent ID, name ID, etc.)
  1063. const idElement = element.querySelector('[id^="name_"], [id^="link_"], [id^="status_"]');
  1064. if (idElement) {
  1065. return idElement.id;
  1066. }
  1067. return null;
  1068. }
  1069. // Sync selection state between table and grid views
  1070. function syncSelectionState(id, isSelected) {
  1071. if (!id) return;
  1072. // Get ID prefix and suffix
  1073. const parts = id.split('_');
  1074. if (parts.length < 2) return;
  1075. const prefix = parts[0];
  1076. const suffix = parts[1];
  1077. // Get all elements with IDs containing this suffix (both in table and grid)
  1078. const selector = `[id$="_${suffix}"]`;
  1079. const relatedElements = document.querySelectorAll(selector);
  1080. // Find related rows and grid items
  1081. let tableRows = [];
  1082. let gridItems = [];
  1083. relatedElements.forEach(el => {
  1084. // Find containing row
  1085. let row = el.closest('.tr.g1, .tr.g2');
  1086. if (row) {
  1087. tableRows.push(row);
  1088. // Also get the next row (detail row)
  1089. if (row.nextElementSibling && !row.nextElementSibling.classList.contains('g1') &&
  1090. !row.nextElementSibling.classList.contains('g2')) {
  1091. tableRows.push(row.nextElementSibling);
  1092. }
  1093. }
  1094. // Find containing grid item
  1095. let gridItem = el.closest('.torrent-entry');
  1096. if (gridItem) {
  1097. gridItems.push(gridItem);
  1098. }
  1099. });
  1100. // Apply selection state to all related elements
  1101. tableRows = [...new Set(tableRows)]; // Remove duplicates
  1102. tableRows.forEach(row => {
  1103. if (isSelected) {
  1104. row.classList.add('selected');
  1105. } else {
  1106. row.classList.remove('selected');
  1107. }
  1108. });
  1109. gridItems = [...new Set(gridItems)]; // Remove duplicates
  1110. gridItems.forEach(item => {
  1111. if (isSelected) {
  1112. item.classList.add('selected');
  1113. } else {
  1114. item.classList.remove('selected');
  1115. }
  1116. });
  1117. }
  1118.  
  1119. function addSwitchToGridLayoutButton() {
  1120. const button = document.createElement('button');
  1121. button.textContent = 'Switch to Grid Layout';
  1122. button.id = 'switchLayoutButton';
  1123. button.style.position = 'fixed';
  1124. button.style.top = '10px';
  1125. button.style.right = '20px';
  1126. button.style.zIndex = '1000';
  1127. button.setAttribute('data-current-layout', 'table');
  1128. button.addEventListener('click', (e) => {
  1129. // Add visual feedback
  1130. addButtonClickFeedback(button);
  1131. toggleLayout();
  1132. });
  1133. document.body.appendChild(button);
  1134. }
  1135.  
  1136. function toggleLayout() {
  1137. const button = document.getElementById('switchLayoutButton');
  1138. const currentLayout = button.getAttribute('data-current-layout');
  1139. if (currentLayout === 'table') {
  1140. // Switch to grid layout
  1141. const columnCount = 3;
  1142. createGridLayout(columnCount);
  1143. button.textContent = 'Switch to Table Layout';
  1144. button.setAttribute('data-current-layout', 'grid');
  1145. } else {
  1146. // Switch back to table layout without reload
  1147. const gridContainer = document.getElementById('torrent-grid-container');
  1148. const originalTable = document.getElementById('original-torrent-table');
  1149. if (gridContainer && originalTable) {
  1150. // Hide grid, show table
  1151. gridContainer.style.display = 'none';
  1152. originalTable.style.display = 'table';
  1153. button.textContent = 'Switch to Grid Layout';
  1154. button.setAttribute('data-current-layout', 'table');
  1155. }
  1156. }
  1157. // Update floating buttons visibility
  1158. updateFloatingButtonsVisibility();
  1159. }
  1160. function switchToGridLayout() {
  1161. const button = document.getElementById('switchLayoutButton');
  1162. if (button.getAttribute('data-current-layout') === 'table') {
  1163. toggleLayout();
  1164. }
  1165. }
  1166.  
  1167. function deleteSelectedTorrents() {
  1168. const selectedItems = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected, .torrent-entry.selected');
  1169. const deleteIds = [];
  1170. selectedItems.forEach(item => {
  1171. // Find delete link within the item
  1172. const deleteLink = item.querySelector('a[href*="del="]');
  1173. if (deleteLink) {
  1174. const href = deleteLink.getAttribute('href');
  1175. const match = href.match(/del=([^&]+)/);
  1176. if (match && match[1]) {
  1177. deleteIds.push(match[1]);
  1178. }
  1179. }
  1180. });
  1181. if (deleteIds.length === 0) return;
  1182. if (confirm(`Delete ${deleteIds.length} selected torrents?`)) {
  1183. // Change button text to "Deleting..." after confirmation
  1184. const originalWidth = deleteButton.offsetWidth;
  1185. deleteButton.textContent = 'Deleting...';
  1186. deleteButton.style.width = `${originalWidth}px`;
  1187. // Process deletions sequentially to avoid overwhelming the server
  1188. deleteSequentially(deleteIds, 0);
  1189. }
  1190. }
  1191.  
  1192. function deleteSequentially(ids, index) {
  1193. if (index >= ids.length) {
  1194. // All done, refresh the page
  1195. window.location.reload();
  1196. return;
  1197. }
  1198. const id = ids[index];
  1199. const xhr = new XMLHttpRequest();
  1200. xhr.open('GET', `?p=1&del=${id}`, true);
  1201. xhr.onload = function() {
  1202. // Move to next deletion
  1203. deleteSequentially(ids, index + 1);
  1204. };
  1205. xhr.onerror = function() {
  1206. // Still try the next one
  1207. deleteSequentially(ids, index + 1);
  1208. };
  1209. xhr.send();
  1210. }
  1211.  
  1212. function setupGridItemsEventHandlers() {
  1213. const entries = document.querySelectorAll('.torrent-entry');
  1214. entries.forEach(entry => {
  1215. // Clear any existing handlers by cloning the node
  1216. const newEntry = entry.cloneNode(true);
  1217. entry.parentNode.replaceChild(newEntry, entry);
  1218. // Add event stopping for buttons in grid view
  1219. const deleteButton = newEntry.querySelector('a[href*="del"]');
  1220. if (deleteButton) {
  1221. deleteButton.addEventListener('click', (e) => {
  1222. e.stopPropagation();
  1223. });
  1224. }
  1225. const downloadButtons = newEntry.querySelectorAll('input[type="image"]');
  1226. downloadButtons.forEach(button => {
  1227. button.addEventListener('click', (e) => {
  1228. e.stopPropagation();
  1229. });
  1230. });
  1231. const fileInfoButtons = newEntry.querySelectorAll('a[rel="facebox"]');
  1232. fileInfoButtons.forEach(button => {
  1233. button.addEventListener('click', (e) => {
  1234. e.stopPropagation();
  1235. });
  1236. });
  1237. // Main click handler for selection toggling
  1238. newEntry.addEventListener('click', (e) => {
  1239. // Prevent click propagation if this is a button
  1240. if (e.target.closest('a[href*="del"]') ||
  1241. e.target.closest('input[type="image"]') ||
  1242. e.target.closest('a[rel="facebox"]')) {
  1243. return;
  1244. }
  1245. // Toggle selection state
  1246. newEntry.classList.toggle('selected');
  1247. if (newEntry.classList.contains('selected')) {
  1248. newEntry.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
  1249. } else {
  1250. newEntry.style.backgroundColor = '';
  1251. }
  1252. // Get ID and sync with table view
  1253. const id = getIdentifierFromElement(newEntry);
  1254. if (id) {
  1255. const isNowSelected = newEntry.classList.contains('selected');
  1256. syncTableViewSelection(id, isNowSelected);
  1257. }
  1258. updateFloatingButtonsVisibility();
  1259. });
  1260. });
  1261. }
  1262. function syncTableViewSelection(id, isSelected) {
  1263. if (!id) return;
  1264. // Get ID suffix
  1265. const parts = id.split('_');
  1266. if (parts.length < 2) return;
  1267. const suffix = parts[1];
  1268. // Find table rows with this torrent ID
  1269. const selector = `[id$="_${suffix}"]`;
  1270. const originalTable = document.getElementById('original-torrent-table');
  1271. if (!originalTable) return;
  1272. const elements = originalTable.querySelectorAll(selector);
  1273. elements.forEach(el => {
  1274. const row = el.closest('.tr.g1, .tr.g2');
  1275. if (row) {
  1276. // Set selection state on main row
  1277. if (isSelected) {
  1278. row.classList.add('selected');
  1279. } else {
  1280. row.classList.remove('selected');
  1281. }
  1282. // Set selection state on detail row
  1283. const nextRow = row.nextElementSibling;
  1284. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  1285. if (isSelected) {
  1286. nextRow.classList.add('selected');
  1287. } else {
  1288. nextRow.classList.remove('selected');
  1289. }
  1290. }
  1291. }
  1292. });
  1293. }
  1294.  
  1295. // Helper function to add visual feedback to buttons
  1296. function addButtonClickFeedback(button, tempText = null) {
  1297. // Store original text if we're changing it
  1298. const originalText = tempText ? button.textContent : null;
  1299. // Store original width to prevent layout shifts
  1300. const originalWidth = button.offsetWidth;
  1301. // Add animation class
  1302. button.classList.add('button-clicked');
  1303. // Change text if specified
  1304. if (tempText) {
  1305. button.textContent = tempText;
  1306. // Ensure width doesn't change
  1307. button.style.width = `${originalWidth}px`;
  1308. }
  1309. // Remove animation class and restore text after animation
  1310. setTimeout(() => {
  1311. button.classList.remove('button-clicked');
  1312. if (originalText) {
  1313. button.textContent = originalText;
  1314. // Remove explicit width to allow natural sizing again
  1315. button.style.width = '';
  1316. }
  1317. }, 500);
  1318. }
  1319.  
  1320. if (document.readyState === 'complete') {
  1321. initializeApplication();
  1322. } else {
  1323. window.addEventListener('load', initializeApplication);
  1324. }
  1325. })();